diff --git a/.eslintrc.js b/.eslintrc.js index bcbfb44b7f8c..89578fa19033 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -267,7 +267,7 @@ module.exports = { 'app/scripts/controllers/app-state.test.js', 'app/scripts/controllers/network/**/*.test.js', 'app/scripts/controllers/network/**/*.test.ts', - 'app/scripts/controllers/network/provider-api-tests/*.js', + 'app/scripts/controllers/network/provider-api-tests/*.ts', 'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/lib/**/*.test.js', 'app/scripts/migrations/*.test.js', diff --git a/.storybook/test-data.js b/.storybook/test-data.js index 3f133fcba5e3..23c88c48568f 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -263,12 +263,12 @@ const state = { enabled: true, id: 'local:http://localhost:8080/', initialPermissions: { - snap_confirm: {}, + snap_dialog: {}, }, manifest: { description: 'An example MetaMask Snap.', initialPermissions: { - snap_confirm: {}, + snap_dialog: {}, }, manifestVersion: '0.1', proposedName: 'MetaMask Example Snap', @@ -298,7 +298,7 @@ const state = { enabled: true, id: 'npm:http://localhost:8080/', initialPermissions: { - snap_confirm: {}, + snap_dialog: {}, eth_accounts: {}, snap_manageState: {}, }, @@ -306,7 +306,7 @@ const state = { description: 'This swap provides developers everywhere access to an entirely new data storage paradigm, even letting your programs store data autonomously. Learn more.', initialPermissions: { - snap_confirm: {}, + snap_dialog: {}, eth_accounts: {}, snap_manageState: {}, }, @@ -606,13 +606,6 @@ const state = { rpcUrl: '', chainId: '0x5', }, - previousProviderStore: { - type: 'goerli', - ticker: 'ETH', - nickname: '', - rpcUrl: '', - chainId: '0x5', - }, network: '5', accounts: { '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { @@ -1356,9 +1349,9 @@ const state = { }, 'local:http://localhost:8080/': { permissions: { - snap_confirm: { + snap_dialog: { invoker: 'local:http://localhost:8080/', - parentCapability: 'snap_confirm', + parentCapability: 'snap_dialog', id: 'a7342F4b-beae-4525-a36c-c0635fd03359', date: 1620710693178, caveats: [], diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index c63d2a78d8b8..d3d78c71d6d0 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -2697,10 +2697,6 @@ "message": "Regelmäßige Transaktionen planen und ausführen.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Bestätigung in MetaMask anzeigen.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Dialogfenster in MetaMask anzeigen.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 3695fede50ac..f5abc4850c4b 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -2697,10 +2697,6 @@ "message": "Προγραμματισμός και εκτέλεση περιοδικών ενεργειών.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Εμφάνιση επιβεβαίωσης στο MetaMask.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Εμφάνιση παραθύρων διαλόγου στο MetaMask.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 382dd314ab87..03f2c2de29d2 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -109,6 +109,9 @@ "about": { "message": "About" }, + "accept": { + "message": "Accept" + }, "acceptTermsOfUse": { "message": "I have read and agree to the $1", "description": "$1 is the `terms` message" @@ -305,6 +308,10 @@ "advancedPriorityFeeToolTip": { "message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction." }, + "agreeTermsOfUse": { + "message": "I agree to Metamask's $1", + "description": "$1 is the `terms` link" + }, "airgapVault": { "message": "AirGap Vault" }, @@ -929,6 +936,21 @@ "custodianAccount": { "message": "Custodian account" }, + "custodyRefreshTokenModalDescription": { + "message": "Please go to $1 and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again." + }, + "custodyRefreshTokenModalDescription1": { + "message": "Your custodian issues a token that authenticates the MetaMask Institutional extension, allowing you to connect your accounts." + }, + "custodyRefreshTokenModalDescription2": { + "message": "This token expires after a certain period for security reasons. This requires you to reconnect to MMI." + }, + "custodyRefreshTokenModalSubtitle": { + "message": "Why am I seeing this?" + }, + "custodyRefreshTokenModalTitle": { + "message": "Your custodian session has expired" + }, "custom": { "message": "Advanced" }, @@ -2892,14 +2914,6 @@ "message": "Allow the snap to perform actions that run periodically at fixed times, dates, or intervals. This can be used to trigger time-sensitive interactions or notifications.", "description": "An extended description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Display a confirmation in MetaMask.", - "description": "The description for the `snap_confirm` permission" - }, - "permission_customConfirmationDescription": { - "message": "Allow the snap to display MetaMask popups with custom text, and buttons to approve or reject an action.", - "description": "An extended description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Display dialog windows in MetaMask.", "description": "The description for the `snap_dialog` permission" @@ -4295,6 +4309,15 @@ "termsOfUse": { "message": "terms of use" }, + "termsOfUseAgreeText": { + "message": " I agree to the Terms of Use, which apply to my use of MetaMask and all of its features" + }, + "termsOfUseFooterText": { + "message": "Please scroll to read all sections" + }, + "termsOfUseTitle": { + "message": "Our Terms of Use have updated" + }, "testNetworks": { "message": "Test networks" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 39e7a957311e..46e67cc8976b 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -2697,10 +2697,6 @@ "message": "Programar y ejecutar acciones periódicas.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Mostrar una confirmación en MetaMask.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Mostrar ventanas de diálogo en MetaMask.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index af623de01a7f..74c083f1c780 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -2697,10 +2697,6 @@ "message": "Planifiez et exécutez des actions périodiques.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Afficher une confirmation dans MetaMask.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Afficher les boîtes de dialogue dans MetaMask.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index ec81fdc4f28a..9e3f926bdc87 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -2697,10 +2697,6 @@ "message": "समय-समय पर आने वाले क्रियाओं को शेड्यूल और निष्पादित करें।", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "MetaMask में पुष्टि को दर्शाएं।", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "MetaMask में डायलॉग विंडो प्रदर्शित करें।", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 30823b2d1620..edeaeae14e4b 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -2697,10 +2697,6 @@ "message": "Jadwalkan dan lakukan tindakan berkala.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Tampilkan konfirmasi di MetaMask.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Tampilkan jendela dialog di MetaMask.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 2ac044508587..f59ed9799ffc 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -2697,10 +2697,6 @@ "message": "定期的なアクションのスケジュール設定と実行。", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "MetaMask に確認を表示します。", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "MetaMask にダイアログウィンドウを表示します。", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index c82644e4be54..5a1f814a98b1 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -2697,10 +2697,6 @@ "message": "정기적 활동 예약 및 실행", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "MetaMask에 확인을 표시합니다.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "MetaMask 대화창 표시", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index a9544c37892f..76d7f1fca57d 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -2697,10 +2697,6 @@ "message": "Agende e execute ações periódicas.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Exibir uma confirmação na MetaMask.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Exibir janelas de diálogo na MetaMask.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 8c560db82103..b6dd4412aaf4 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -2697,10 +2697,6 @@ "message": "Планируйте и выполняйте периодические действия.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Показать подтверждение в MetaMask.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Отображение диалоговых окон в MetaMask.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index f8747ac19c12..f0d540268ec9 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -2697,10 +2697,6 @@ "message": "Mag-iskedyul at magsagawa ng mga pana-panahong mga aksyon.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Ipakita ang kumpirmasyon sa MetaMask.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Ipakita ang mga dialog window sa MetaMask.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 96038a2209ac..24378c56117f 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -2697,10 +2697,6 @@ "message": "Periyodik eylemleri planla ve gerçekleştir.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "MetaMask'te bir onay görüntüle.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "MetaMask'te iletişim kutusu pencerelerini göster.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 10dde0e64c6d..ee8e37dbd568 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -2697,10 +2697,6 @@ "message": "Lên lịch và thực hiện các hành động theo định kỳ.", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "Hiển thị xác nhận trong MetaMask.", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "Hiển thị cửa sổ hộp thoại trong MetaMask.", "description": "The description for the `snap_dialog` permission" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 9eee48c84a10..8f160cb99195 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -2697,10 +2697,6 @@ "message": "规划并执行定期操作。", "description": "The description for the `snap_cronjob` permission" }, - "permission_customConfirmation": { - "message": "在MetaMask中显示确认。", - "description": "The description for the `snap_confirm` permission" - }, "permission_dialog": { "message": "在 MetaMask 中显示对话框窗口。", "description": "The description for the `snap_dialog` permission" diff --git a/app/scripts/background.js b/app/scripts/background.js index 622e888f9630..048d494f84f3 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -208,6 +208,7 @@ browser.runtime.onConnectExternal.addListener(async (...args) => { * @property {boolean} isInitialized - Whether the first vault has been created. * @property {boolean} isUnlocked - Whether the vault is currently decrypted and accounts are available for selection. * @property {boolean} isAccountMenuOpen - Represents whether the main account selection UI is currently displayed. + * @property {boolean} isNetworkMenuOpen - Represents whether the main network selection UI is currently displayed. * @property {object} identities - An object matching lower-case hex addresses to Identity objects with "address" and "name" (nickname) keys. * @property {object} unapprovedTxs - An object mapping transaction hashes to unapproved transactions. * @property {object} networkConfigurations - A list of network configurations, containing RPC provider details (eg chainId, rpcUrl, rpcPreferences). diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index f7669e055bcd..cf61daff6292 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -1,5 +1,7 @@ import EventEmitter from 'events'; import { ObservableStore } from '@metamask/obs-store'; +import { v4 as uuid } from 'uuid'; +import log from 'loglevel'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; import { MINUTE } from '../../../shared/constants/time'; import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms'; @@ -8,8 +10,11 @@ import { isBeta } from '../../../ui/helpers/utils/build-types'; import { ENVIRONMENT_TYPE_BACKGROUND, POLLING_TOKEN_ENVIRONMENT_TYPES, + ORIGIN_METAMASK, } from '../../../shared/constants/app'; +const APPROVAL_REQUEST_TYPE = 'unlock'; + export default class AppStateController extends EventEmitter { /** * @param {object} opts @@ -20,9 +25,9 @@ export default class AppStateController extends EventEmitter { isUnlocked, initState, onInactiveTimeout, - showUnlockRequest, preferencesStore, qrHardwareStore, + messenger, } = opts; super(); @@ -59,8 +64,6 @@ export default class AppStateController extends EventEmitter { this.waitingForUnlock = []; addUnlockListener(this.handleUnlock.bind(this)); - this._showUnlockRequest = showUnlockRequest; - preferencesStore.subscribe(({ preferences }) => { const currentState = this.store.getState(); if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) { @@ -74,6 +77,9 @@ export default class AppStateController extends EventEmitter { const { preferences } = preferencesStore.getState(); this._setInactiveTimeout(preferences.autoLockTimeLimit); + + this.messagingSystem = messenger; + this._approvalRequestId = null; } /** @@ -108,7 +114,7 @@ export default class AppStateController extends EventEmitter { this.waitingForUnlock.push({ resolve }); this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); if (shouldShowUnlockRequest) { - this._showUnlockRequest(); + this._requestApproval(); } } @@ -122,6 +128,8 @@ export default class AppStateController extends EventEmitter { } this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); } + + this._acceptApproval(); } /** @@ -164,6 +172,17 @@ export default class AppStateController extends EventEmitter { }); } + /** + * Record the timestamp of the last time the user has acceoted the terms of use + * + * @param {number} lastAgreed - timestamp when user last accepted the terms of use + */ + setTermsOfUseLastAgreed(lastAgreed) { + this.store.updateState({ + termsOfUseLastAgreed: lastAgreed, + }); + } + /** * Record the timestamp of the last time the user has seen the outdated browser warning * @@ -369,4 +388,39 @@ export default class AppStateController extends EventEmitter { serviceWorkerLastActiveTime, }); } + + _requestApproval() { + this._approvalRequestId = uuid(); + + this.messagingSystem + .call( + 'ApprovalController:addRequest', + { + id: this._approvalRequestId, + origin: ORIGIN_METAMASK, + type: APPROVAL_REQUEST_TYPE, + }, + true, + ) + .catch(() => { + // Intentionally ignored as promise not currently used + }); + } + + _acceptApproval() { + if (!this._approvalRequestId) { + log.error('Attempted to accept missing unlock approval request'); + return; + } + try { + this.messagingSystem.call( + 'ApprovalController:acceptRequest', + this._approvalRequestId, + ); + } catch (error) { + log.error('Failed to accept transaction approval request', error); + } + + this._approvalRequestId = null; + } } diff --git a/app/scripts/controllers/app-state.test.js b/app/scripts/controllers/app-state.test.js index 7aa27e44b1c1..02d3cda3c789 100644 --- a/app/scripts/controllers/app-state.test.js +++ b/app/scripts/controllers/app-state.test.js @@ -1,12 +1,158 @@ +import { ObservableStore } from '@metamask/obs-store'; +import log from 'loglevel'; +import { ORIGIN_METAMASK } from '../../../shared/constants/app'; import AppStateController from './app-state'; +jest.mock('loglevel'); + +let appStateController, mockStore; + describe('AppStateController', () => { + mockStore = new ObservableStore(); + const createAppStateController = (initState = {}) => { + return new AppStateController({ + addUnlockListener: jest.fn(), + isUnlocked: jest.fn(() => true), + initState, + onInactiveTimeout: jest.fn(), + showUnlockRequest: jest.fn(), + preferencesStore: { + subscribe: jest.fn(), + getState: jest.fn(() => ({ + preferences: { + autoLockTimeLimit: 0, + }, + })), + }, + qrHardwareStore: { + subscribe: jest.fn(), + }, + messenger: { + call: jest.fn(() => ({ + catch: jest.fn(), + })), + }, + }); + }; + + beforeEach(() => { + appStateController = createAppStateController({ store: mockStore }); + }); + describe('setOutdatedBrowserWarningLastShown', () => { - it('should set the last shown time', () => { - const appStateController = new AppStateController({ + it('sets the last shown time', () => { + appStateController = createAppStateController(); + const date = new Date(); + + appStateController.setOutdatedBrowserWarningLastShown(date); + + expect( + appStateController.store.getState().outdatedBrowserWarningLastShown, + ).toStrictEqual(date); + }); + + it('sets outdated browser warning last shown timestamp', () => { + const lastShownTimestamp = Date.now(); + appStateController = createAppStateController(); + const updateStateSpy = jest.spyOn( + appStateController.store, + 'updateState', + ); + + appStateController.setOutdatedBrowserWarningLastShown(lastShownTimestamp); + + expect(updateStateSpy).toHaveBeenCalledTimes(1); + expect(updateStateSpy).toHaveBeenCalledWith({ + outdatedBrowserWarningLastShown: lastShownTimestamp, + }); + + updateStateSpy.mockRestore(); + }); + }); + + describe('getUnlockPromise', () => { + it('waits for unlock if the extension is locked', async () => { + appStateController = createAppStateController(); + const isUnlockedMock = jest + .spyOn(appStateController, 'isUnlocked') + .mockReturnValue(false); + const waitForUnlockSpy = jest.spyOn(appStateController, 'waitForUnlock'); + + appStateController.getUnlockPromise(true); + expect(isUnlockedMock).toHaveBeenCalled(); + expect(waitForUnlockSpy).toHaveBeenCalledWith(expect.any(Function), true); + }); + + it('resolves immediately if the extension is already unlocked', async () => { + appStateController = createAppStateController(); + const isUnlockedMock = jest + .spyOn(appStateController, 'isUnlocked') + .mockReturnValue(true); + + await expect( + appStateController.getUnlockPromise(false), + ).resolves.toBeUndefined(); + + expect(isUnlockedMock).toHaveBeenCalled(); + }); + }); + + describe('waitForUnlock', () => { + it('resolves immediately if already unlocked', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + const resolveFn = jest.fn(); + appStateController.waitForUnlock(resolveFn, false); + expect(emitSpy).toHaveBeenCalledWith('updateBadge'); + expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(0); + }); + + it('creates approval request when waitForUnlock is called with shouldShowUnlockRequest as true', async () => { + jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false); + + const resolveFn = jest.fn(); + appStateController.waitForUnlock(resolveFn, true); + + expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(1); + expect(appStateController.messagingSystem.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + expect.objectContaining({ + id: expect.any(String), + origin: ORIGIN_METAMASK, + type: 'unlock', + }), + true, + ); + }); + }); + + describe('handleUnlock', () => { + beforeEach(() => { + jest.spyOn(appStateController, 'isUnlocked').mockReturnValue(false); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('accepts approval request revolving all the related promises', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + const resolveFn = jest.fn(); + appStateController.waitForUnlock(resolveFn, true); + + appStateController.handleUnlock(); + + expect(emitSpy).toHaveBeenCalled(); + expect(emitSpy).toHaveBeenCalledWith('updateBadge'); + expect(appStateController.messagingSystem.call).toHaveBeenCalled(); + expect(appStateController.messagingSystem.call).toHaveBeenCalledWith( + 'ApprovalController:acceptRequest', + expect.any(String), + ); + }); + + it('logs if rejecting approval request throws', async () => { + appStateController._approvalRequestId = 'mock-approval-request-id'; + appStateController = new AppStateController({ addUnlockListener: jest.fn(), isUnlocked: jest.fn(() => true), - initState: {}, onInactiveTimeout: jest.fn(), showUnlockRequest: jest.fn(), preferencesStore: { @@ -20,14 +166,184 @@ describe('AppStateController', () => { qrHardwareStore: { subscribe: jest.fn(), }, + messenger: { + call: jest.fn(() => { + throw new Error('mock error'); + }), + }, }); - const date = new Date(); - appStateController.setOutdatedBrowserWarningLastShown(date); + appStateController.handleUnlock(); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith( + 'Attempted to accept missing unlock approval request', + ); + }); + + it('returns without call messenger if no approval request in pending', async () => { + const emitSpy = jest.spyOn(appStateController, 'emit'); + + appStateController.handleUnlock(); + + expect(emitSpy).toHaveBeenCalledTimes(0); + expect(appStateController.messagingSystem.call).toHaveBeenCalledTimes(0); + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith( + 'Attempted to accept missing unlock approval request', + ); + }); + }); + + describe('setDefaultHomeActiveTabName', () => { + it('sets the default home tab name', () => { + appStateController.setDefaultHomeActiveTabName('testTabName'); + expect(appStateController.store.getState().defaultHomeActiveTabName).toBe( + 'testTabName', + ); + }); + }); + describe('setConnectedStatusPopoverHasBeenShown', () => { + it('sets connected status popover as shown', () => { + appStateController.setConnectedStatusPopoverHasBeenShown(); expect( - appStateController.store.getState().outdatedBrowserWarningLastShown, - ).toStrictEqual(date); + appStateController.store.getState().connectedStatusPopoverHasBeenShown, + ).toBe(true); + }); + }); + + describe('setRecoveryPhraseReminderHasBeenShown', () => { + it('sets recovery phrase reminder as shown', () => { + appStateController.setRecoveryPhraseReminderHasBeenShown(); + expect( + appStateController.store.getState().recoveryPhraseReminderHasBeenShown, + ).toBe(true); + }); + }); + + describe('setRecoveryPhraseReminderLastShown', () => { + it('sets the last shown time of recovery phrase reminder', () => { + const timestamp = Date.now(); + appStateController.setRecoveryPhraseReminderLastShown(timestamp); + + expect( + appStateController.store.getState().recoveryPhraseReminderLastShown, + ).toBe(timestamp); + }); + }); + + describe('setLastActiveTime', () => { + it('sets the last active time to the current time', () => { + const spy = jest.spyOn(appStateController, '_resetTimer'); + appStateController.setLastActiveTime(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('setBrowserEnvironment', () => { + it('sets the current browser and OS environment', () => { + appStateController.setBrowserEnvironment('Windows', 'Chrome'); + expect( + appStateController.store.getState().browserEnvironment, + ).toStrictEqual({ + os: 'Windows', + browser: 'Chrome', + }); + }); + }); + + describe('addPollingToken', () => { + it('adds a pollingToken for a given environmentType', () => { + const pollingTokenType = 'popupGasPollTokens'; + appStateController.addPollingToken('token1', pollingTokenType); + expect(appStateController.store.getState()[pollingTokenType]).toContain( + 'token1', + ); + }); + }); + + describe('removePollingToken', () => { + it('removes a pollingToken for a given environmentType', () => { + const pollingTokenType = 'popupGasPollTokens'; + appStateController.addPollingToken('token1', pollingTokenType); + appStateController.removePollingToken('token1', pollingTokenType); + expect( + appStateController.store.getState()[pollingTokenType], + ).not.toContain('token1'); + }); + }); + + describe('clearPollingTokens', () => { + it('clears all pollingTokens', () => { + appStateController.addPollingToken('token1', 'popupGasPollTokens'); + appStateController.addPollingToken('token2', 'notificationGasPollTokens'); + appStateController.addPollingToken('token3', 'fullScreenGasPollTokens'); + appStateController.clearPollingTokens(); + + expect( + appStateController.store.getState().popupGasPollTokens, + ).toStrictEqual([]); + expect( + appStateController.store.getState().notificationGasPollTokens, + ).toStrictEqual([]); + expect( + appStateController.store.getState().fullScreenGasPollTokens, + ).toStrictEqual([]); + }); + }); + + describe('setShowTestnetMessageInDropdown', () => { + it('sets whether the testnet dismissal link should be shown in the network dropdown', () => { + appStateController.setShowTestnetMessageInDropdown(true); + expect( + appStateController.store.getState().showTestnetMessageInDropdown, + ).toBe(true); + + appStateController.setShowTestnetMessageInDropdown(false); + expect( + appStateController.store.getState().showTestnetMessageInDropdown, + ).toBe(false); + }); + }); + + describe('setShowBetaHeader', () => { + it('sets whether the beta notification heading on the home page', () => { + appStateController.setShowBetaHeader(true); + expect(appStateController.store.getState().showBetaHeader).toBe(true); + + appStateController.setShowBetaHeader(false); + expect(appStateController.store.getState().showBetaHeader).toBe(false); + }); + }); + + describe('setCurrentPopupId', () => { + it('sets the currentPopupId in the appState', () => { + const popupId = 'popup1'; + + appStateController.setCurrentPopupId(popupId); + expect(appStateController.store.getState().currentPopupId).toBe(popupId); + }); + }); + + describe('getCurrentPopupId', () => { + it('retrieves the currentPopupId saved in the appState', () => { + const popupId = 'popup1'; + + appStateController.setCurrentPopupId(popupId); + expect(appStateController.getCurrentPopupId()).toBe(popupId); + }); + }); + + describe('setFirstTimeUsedNetwork', () => { + it('updates the array of the first time used networks', () => { + const chainId = '0x1'; + + appStateController.setFirstTimeUsedNetwork(chainId); + expect(appStateController.store.getState().usedNetworks[chainId]).toBe( + true, + ); }); }); }); diff --git a/app/scripts/controllers/network/create-network-client.test.js b/app/scripts/controllers/network/create-network-client.test.ts similarity index 100% rename from app/scripts/controllers/network/create-network-client.test.js rename to app/scripts/controllers/network/create-network-client.test.ts diff --git a/app/scripts/controllers/network/network-controller.test.ts b/app/scripts/controllers/network/network-controller.test.ts index 5e848830a752..e1cf8edcb68d 100644 --- a/app/scripts/controllers/network/network-controller.test.ts +++ b/app/scripts/controllers/network/network-controller.test.ts @@ -647,12 +647,6 @@ describe('NetworkController', () => { }, "networkId": null, "networkStatus": "unknown", - "previousProviderStore": { - "chainId": "0x9999", - "nickname": "Test initial state", - "rpcUrl": "http://example-custom-rpc.metamask.io", - "type": "rpc", - }, "provider": { "chainId": "0x9999", "nickname": "Test initial state", @@ -677,13 +671,6 @@ describe('NetworkController', () => { }, "networkId": null, "networkStatus": "unknown", - "previousProviderStore": { - "chainId": "0x539", - "nickname": "Localhost 8545", - "rpcUrl": "http://localhost:8545", - "ticker": "ETH", - "type": "rpc", - }, "provider": { "chainId": "0x539", "nickname": "Localhost 8545", @@ -4116,46 +4103,6 @@ describe('NetworkController', () => { ); }); - it('stores the current provider configuration before overwriting it', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0x111', - ticker: 'TEST', - }, - networkConfigurations: { - testNetworkConfigurationId2: { - id: 'testNetworkConfigurationId2', - rpcUrl: 'https://mock-rpc-url-2', - chainId: '0x222', - ticker: 'ABC', - }, - }, - }, - }, - async ({ controller }) => { - const network = new CustomNetworkCommunications({ - customRpcUrl: 'https://mock-rpc-url-2', - }); - network.mockEssentialRpcCalls(); - - controller.setActiveNetwork('testNetworkConfigurationId2'); - - expect( - controller.store.getState().previousProviderStore, - ).toStrictEqual({ - type: 'rpc', - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0x111', - ticker: 'TEST', - }); - }, - ); - }); - it('overwrites the provider configuration given a networkConfigurationId that matches a configured networkConfiguration', async () => { await withController( { @@ -4638,68 +4585,6 @@ describe('NetworkController', () => { describe('setProviderType', () => { for (const { networkType, chainId, ticker } of INFURA_NETWORKS) { describe(`given a type of "${networkType}"`, () => { - it('stores the current provider configuration before overwriting it', async () => { - await withController( - { - state: { - provider: { - type: 'rpc', - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - nickname: 'test-chain-2', - ticker: 'TEST2', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer-2.com', - }, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url-1', - chainId: '0xtest', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer.com', - }, - id: 'testNetworkConfigurationId1', - }, - testNetworkConfigurationId2: { - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - nickname: 'test-chain-2', - ticker: 'TEST2', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer-2.com', - }, - id: 'testNetworkConfigurationId2', - }, - }, - }, - }, - async ({ controller }) => { - const network = new InfuraNetworkCommunications({ - infuraNetwork: networkType, - }); - network.mockEssentialRpcCalls(); - - controller.setProviderType(networkType); - - expect( - controller.store.getState().previousProviderStore, - ).toStrictEqual({ - type: 'rpc', - rpcUrl: 'http://mock-rpc-url-2', - chainId: '0xtest2', - nickname: 'test-chain-2', - ticker: 'TEST2', - rpcPrefs: { - blockExplorerUrl: 'test-block-explorer-2.com', - }, - }); - }, - ); - }); - it(`overwrites the provider configuration using type: "${networkType}", chainId: "${chainId}", and ticker "${ticker}", clearing rpcUrl and nickname, and removing rpcPrefs`, async () => { await withController( { @@ -5876,8 +5761,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().provider).toStrictEqual({ @@ -5942,8 +5827,8 @@ describe('NetworkController', () => { const networkWillChange = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkWillChange, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -6006,6 +5891,8 @@ describe('NetworkController', () => { // happens before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress controller.rollbackToPreviousProvider(); }, }); @@ -6074,6 +5961,8 @@ describe('NetworkController', () => { // happens before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress controller.rollbackToPreviousProvider(); }, }); @@ -6130,8 +6019,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -6192,8 +6081,8 @@ describe('NetworkController', () => { controller.getProviderAndBlockTracker(); await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); const { provider: providerAfter } = @@ -6253,8 +6142,8 @@ describe('NetworkController', () => { const networkDidChange = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkDidChange, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(networkDidChange).toBeTruthy(); @@ -6318,7 +6207,7 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, operation: async () => { - controller.rollbackToPreviousProvider(); + await controller.rollbackToPreviousProvider(); }, }); @@ -6381,8 +6270,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().networkStatus).toBe( @@ -6438,8 +6327,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, numberOfNetworkDetailsChanges: 2, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().networkDetails).toStrictEqual({ @@ -6523,8 +6412,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().provider).toStrictEqual({ @@ -6590,8 +6479,8 @@ describe('NetworkController', () => { const networkWillChange = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkWillChange, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -6647,6 +6536,8 @@ describe('NetworkController', () => { // happens before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress controller.rollbackToPreviousProvider(); }, }); @@ -6710,6 +6601,8 @@ describe('NetworkController', () => { // happens before networkDidChange count: 1, operation: () => { + // Intentionally not awaited because we want to check state + // while this operation is in-progress controller.rollbackToPreviousProvider(); }, }); @@ -6761,8 +6654,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -6818,8 +6711,8 @@ describe('NetworkController', () => { controller.getProviderAndBlockTracker(); await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); const { provider: providerAfter } = @@ -6874,8 +6767,8 @@ describe('NetworkController', () => { const networkDidChange = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.NetworkDidChange, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(networkDidChange).toBeTruthy(); @@ -6929,8 +6822,8 @@ describe('NetworkController', () => { const infuraIsUnblocked = await waitForPublishedEvents({ messenger: unrestrictedMessenger, eventType: NetworkControllerEventType.InfuraIsUnblocked, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); @@ -6986,8 +6879,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().networkStatus).toBe('unknown'); @@ -7040,8 +6933,8 @@ describe('NetworkController', () => { await waitForLookupNetworkToComplete({ controller, - operation: () => { - controller.rollbackToPreviousProvider(); + operation: async () => { + await controller.rollbackToPreviousProvider(); }, }); expect(controller.store.getState().networkDetails).toStrictEqual({ diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts index 74bd39f42b62..eb98f8494760 100644 --- a/app/scripts/controllers/network/network-controller.ts +++ b/app/scripts/controllers/network/network-controller.ts @@ -267,7 +267,6 @@ type NetworkConfigurations = Record< */ export type NetworkControllerState = { provider: ProviderConfiguration; - previousProviderStore: ProviderConfiguration; networkId: NetworkIdState; networkStatus: NetworkStatus; networkDetails: NetworkDetails; @@ -424,7 +423,7 @@ export class NetworkController extends EventEmitter { * Observable store containing the provider configuration for the previously * configured network. */ - previousProviderStore: ObservableStore; + #previousProviderConfig: ProviderConfiguration; /** * Observable store containing the network ID for the current network or null @@ -489,9 +488,7 @@ export class NetworkController extends EventEmitter { this.providerStore = new ObservableStore( state.provider || buildDefaultProviderConfigState(), ); - this.previousProviderStore = new ObservableStore( - this.providerStore.getState(), - ); + this.#previousProviderConfig = this.providerStore.getState(); this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState()); this.networkStatusStore = new ObservableStore( buildDefaultNetworkStatusState(), @@ -511,7 +508,6 @@ export class NetworkController extends EventEmitter { this.store = new ComposedStore({ provider: this.providerStore, - previousProviderStore: this.previousProviderStore, networkId: this.networkIdStore, networkStatus: this.networkStatusStore, networkDetails: this.networkDetails, @@ -791,10 +787,10 @@ export class NetworkController extends EventEmitter { * different than the initial network (if it is, then this is equivalent to * calling `resetConnection`). */ - rollbackToPreviousProvider(): void { - const config = this.previousProviderStore.getState(); + async rollbackToPreviousProvider() { + const config = this.#previousProviderConfig; this.providerStore.putState(config); - this._switchNetwork(config); + await this._switchNetwork(config); } /** @@ -870,10 +866,10 @@ export class NetworkController extends EventEmitter { * * @param providerConfig - The provider configuration. */ - _setProviderConfig(providerConfig: ProviderConfiguration): void { - this.previousProviderStore.putState(this.providerStore.getState()); + async _setProviderConfig(providerConfig: ProviderConfiguration) { + this.#previousProviderConfig = this.providerStore.getState(); this.providerStore.putState(providerConfig); - this._switchNetwork(providerConfig); + await this._switchNetwork(providerConfig); } /** @@ -904,14 +900,14 @@ export class NetworkController extends EventEmitter { * @param providerConfig - The provider configuration object that specifies * the new network. */ - _switchNetwork(providerConfig: ProviderConfiguration): void { + async _switchNetwork(providerConfig: ProviderConfiguration) { this.messenger.publish(NetworkControllerEventType.NetworkWillChange); this._resetNetworkId(); this._resetNetworkStatus(); this._resetNetworkDetails(); this._configureProvider(providerConfig); this.messenger.publish(NetworkControllerEventType.NetworkDidChange); - this.lookupNetwork(); + await this.lookupNetwork(); } /** diff --git a/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.js b/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.ts similarity index 96% rename from app/scripts/controllers/network/provider-api-tests/block-hash-in-response.js rename to app/scripts/controllers/network/provider-api-tests/block-hash-in-response.ts index c1778f6283b2..4ee0f633dc9b 100644 --- a/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.js +++ b/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.ts @@ -1,6 +1,15 @@ /* eslint-disable jest/require-top-level-describe, jest/no-export */ -import { withMockedCommunications, withNetworkClient } from './helpers'; +import { + ProviderType, + withMockedCommunications, + withNetworkClient, +} from './helpers'; + +type TestsForRpcMethodThatCheckForBlockHashInResponseOptions = { + providerType: ProviderType; + numberOfParameters: number; +}; /** * Defines tests which exercise the behavior exhibited by an RPC method that @@ -15,8 +24,11 @@ import { withMockedCommunications, withNetworkClient } from './helpers'; * either `infura` or `custom` (default: "infura"). */ export function testsForRpcMethodsThatCheckForBlockHashInResponse( - method, - { numberOfParameters, providerType }, + method: string, + { + numberOfParameters, + providerType, + }: TestsForRpcMethodThatCheckForBlockHashInResponseOptions, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( diff --git a/app/scripts/controllers/network/provider-api-tests/block-param.js b/app/scripts/controllers/network/provider-api-tests/block-param.ts similarity index 99% rename from app/scripts/controllers/network/provider-api-tests/block-param.js rename to app/scripts/controllers/network/provider-api-tests/block-param.ts index 49bd6e772a0b..92fb65f0450d 100644 --- a/app/scripts/controllers/network/provider-api-tests/block-param.js +++ b/app/scripts/controllers/network/provider-api-tests/block-param.ts @@ -3,6 +3,7 @@ import { buildMockParams, buildRequestWithReplacedBlockParam, + ProviderType, waitForPromiseToBeFulfilledAfterRunningAllTimers, withMockedCommunications, withNetworkClient, @@ -13,6 +14,12 @@ import { buildJsonRpcEngineEmptyResponseErrorMessage, } from './shared-tests'; +type TestsForRpcMethodSupportingBlockParam = { + providerType: ProviderType; + blockParamIndex: number; + numberOfParameters: number; +}; + /** * Defines tests which exercise the behavior exhibited by an RPC method that * takes a block parameter. The value of this parameter can be either a block @@ -28,8 +35,12 @@ import { */ /* eslint-disable-next-line jest/no-export */ export function testsForRpcMethodSupportingBlockParam( - method, - { blockParamIndex, numberOfParameters, providerType }, + method: string, + { + blockParamIndex, + numberOfParameters, + providerType, + }: TestsForRpcMethodSupportingBlockParam, ) { describe.each([ ['given no block tag', undefined], @@ -1718,9 +1729,9 @@ export function testsForRpcMethodSupportingBlockParam( [ ['less than the current block number', '0x200'], ['equal to the curent block number', '0x100'], - ], + ] as any, '%s', - (_nestedDesc, currentBlockNumber) => { + (_nestedDesc: string, currentBlockNumber: string) => { it('makes an additional request to the RPC endpoint', async () => { await withMockedCommunications({ providerType }, async (comms) => { const request = { diff --git a/app/scripts/controllers/network/provider-api-tests/helpers.js b/app/scripts/controllers/network/provider-api-tests/helpers.ts similarity index 56% rename from app/scripts/controllers/network/provider-api-tests/helpers.js rename to app/scripts/controllers/network/provider-api-tests/helpers.ts index afa91a7bba6c..edcc1ca0258f 100644 --- a/app/scripts/controllers/network/provider-api-tests/helpers.js +++ b/app/scripts/controllers/network/provider-api-tests/helpers.ts @@ -1,14 +1,13 @@ -import nock from 'nock'; +import nock, { Scope as NockScope } from 'nock'; import sinon from 'sinon'; +import type { JSONRPCResponse } from '@json-rpc-specification/meta-schema'; import EthQuery from 'eth-query'; -import { createNetworkClient } from '../create-network-client'; - -/** - * @typedef {import('nock').Scope} NockScope - * - * A object returned by the `nock` function for mocking requests to a particular - * base URL. - */ +import { Hex } from '@metamask/utils'; +import { BuiltInInfuraNetwork } from '../../../../../shared/constants/network'; +import { + createNetworkClient, + NetworkClientType, +} from '../create-network-client'; /** * A dummy value for the `infuraProjectId` option that `createInfuraClient` @@ -41,9 +40,9 @@ const originalSetTimeout = setTimeout; * keeps failing, you can set `process.env.DEBUG_PROVIDER_TESTS` to `1`. This * will turn on some extra logging. * - * @param {any[]} args - The arguments that `console.log` takes. + * @param args - The arguments that `console.log` takes. */ -function debug(...args) { +function debug(...args: any) { if (process.env.DEBUG_PROVIDER_TESTS === '1') { console.log(...args); } @@ -52,96 +51,89 @@ function debug(...args) { /** * Builds a Nock scope object for mocking provider requests. * - * @param {string} rpcUrl - The URL of the RPC endpoint. - * @returns {NockScope} The nock scope. + * @param rpcUrl - The URL of the RPC endpoint. + * @returns The nock scope. */ -function buildScopeForMockingRequests(rpcUrl) { +function buildScopeForMockingRequests(rpcUrl: string): NockScope { return nock(rpcUrl).filteringRequestBody((body) => { debug('Nock Received Request: ', body); return body; }); } -/** - * @typedef {{ nockScope: NockScope, blockNumber: string }} MockBlockTrackerRequestOptions - * - * The options to `mockNextBlockTrackerRequest` and `mockAllBlockTrackerRequests`. - */ - -/** - * Mocks the next request for the latest block that the block tracker will make. - * - * @param {MockBlockTrackerRequestOptions} args - The arguments. - * @param {NockScope} args.nockScope - A nock scope (a set of mocked requests - * scoped to a certain base URL). - * @param {string} args.blockNumber - The block number that the block tracker - * should report, as a 0x-prefixed hex string. - */ -async function mockNextBlockTrackerRequest({ - nockScope, - blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, -}) { - await mockRpcCall({ - nockScope, - request: { method: 'eth_blockNumber', params: [] }, - response: { result: blockNumber }, - }); -} - -/** - * Mocks all requests for the latest block that the block tracker will make. - * - * @param {MockBlockTrackerRequestOptions} args - The arguments. - * @param {NockScope} args.nockScope - A nock scope (a set of mocked requests - * scoped to a certain base URL). - * @param {string} args.blockNumber - The block number that the block tracker - * should report, as a 0x-prefixed hex string. - */ -async function mockAllBlockTrackerRequests({ - nockScope, - blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, -}) { - await mockRpcCall({ - nockScope, - request: { method: 'eth_blockNumber', params: [] }, - response: { result: blockNumber }, - }).persist(); -} - -/** - * @typedef {{ nockScope: NockScope, request: object, response: object, delay?: number }} MockRpcCallOptions - * - * The options to `mockRpcCall`. - */ +type Request = { method: string; params?: any[] }; +type Response = { + id?: number | string; + jsonrpc?: '2.0'; + error?: any; + result?: any; + httpStatus?: number; +}; +type ResponseBody = { body: JSONRPCResponse }; +type BodyOrResponse = ResponseBody | Response; +type CurriedMockRpcCallOptions = { + request: Request; + // The response data. + response?: BodyOrResponse; + /** + * An error to throw while making the request. + * Takes precedence over `response`. + */ + error?: Error | string; + /** + * The amount of time that should pass before the + * request resolves with the response. + */ + delay?: number; + /** + * The number of times that the request is + * expected to be made. + */ + times?: number; +}; + +type MockRpcCallOptions = { + // A nock scope (a set of mocked requests scoped to a certain base URL). + nockScope: nock.Scope; +} & CurriedMockRpcCallOptions; + +type MockRpcCallResult = nock.Interceptor | nock.Scope; /** * Mocks a JSON-RPC request sent to the provider with the given response. * Provider type is inferred from the base url set on the nockScope. * - * @param {MockRpcCallOptions} args - The arguments. - * @param {NockScope} args.nockScope - A nock scope (a set of mocked requests - * scoped to a certain base URL). - * @param {object} args.request - The request data. - * @param {{body: string} | {httpStatus?: number; id?: number; method?: string; params?: string[]}} [args.response] - Information - * concerning the response that the request should have. If a `body` property is - * present, this is taken as the complete response body. If an `httpStatus` - * property is present, then it is taken as the HTTP status code to respond - * with. Properties other than these two are used to build a complete response - * body (including `id` and `jsonrpc` properties). - * @param {Error | string} [args.error] - An error to throw while making the - * request. Takes precedence over `response`. - * @param {number} [args.delay] - The amount of time that should pass before the - * request resolves with the response. - * @param {number} [args.times] - The number of times that the request is - * expected to be made. - * @returns {NockScope} The nock scope. + * @param args - The arguments. + * @param args.nockScope - A nock scope (a set of mocked requests scoped to a + * certain base URL). + * @param args.request - The request data. + * @param args.response - Information concerning the response that the request + * should have. If a `body` property is present, this is taken as the complete + * response body. If an `httpStatus` property is present, then it is taken as + * the HTTP status code to respond with. Properties other than these two are + * used to build a complete response body (including `id` and `jsonrpc` + * properties). + * @param args.error - An error to throw while making the request. Takes + * precedence over `response`. + * @param args.delay - The amount of time that should pass before the request + * resolves with the response. + * @param args.times - The number of times that the request is expected to be + * made. + * @returns The nock scope. */ -function mockRpcCall({ nockScope, request, response, error, delay, times }) { +function mockRpcCall({ + nockScope, + request, + response, + error, + delay, + times, +}: MockRpcCallOptions): MockRpcCallResult { // eth-query always passes `params`, so even if we don't supply this property, // for consistency with makeRpcCall, assume that the `body` contains it const { method, params = [], ...rest } = request; let httpStatus = 200; - let completeResponse = { id: 2, jsonrpc: '2.0' }; + let completeResponse: JSONRPCResponse = { id: 2, jsonrpc: '2.0' }; if (response !== undefined) { if ('body' in response) { completeResponse = response.body; @@ -156,6 +148,7 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) { } } } + /* @ts-expect-error The types for Nock do not include `basePath` in the interface for Nock.Scope. */ const url = nockScope.basePath.includes('infura.io') ? `/v3/${MOCK_INFURA_PROJECT_ID}` : '/'; @@ -189,7 +182,7 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) { if (error !== undefined) { return nockRequest.replyWithError(error); } else if (completeResponse !== undefined) { - return nockRequest.reply(httpStatus, (_, requestBody) => { + return nockRequest.reply(httpStatus, (_, requestBody: any) => { if (response !== undefined && !('body' in response)) { if (response.id === undefined) { completeResponse.id = requestBody.id; @@ -204,16 +197,72 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) { return nockRequest; } +type MockBlockTrackerRequestOptions = { + /** + * A nock scope (a set of mocked requests scoped to a certain base url). + */ + nockScope: NockScope; + /** + * The block number that the block tracker should report, as a 0x-prefixed hex + * string. + */ + blockNumber: string; +}; + +/** + * Mocks the next request for the latest block that the block tracker will make. + * + * @param args - The arguments. + * @param args.nockScope - A nock scope (a set of mocked requests scoped to a + * certain base URL). + * @param args.blockNumber - The block number that the block tracker should + * report, as a 0x-prefixed hex string. + */ +function mockNextBlockTrackerRequest({ + nockScope, + blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, +}: MockBlockTrackerRequestOptions) { + mockRpcCall({ + nockScope, + request: { method: 'eth_blockNumber', params: [] }, + response: { result: blockNumber }, + }); +} + +/** + * Mocks all requests for the latest block that the block tracker will make. + * + * @param args - The arguments. + * @param args.nockScope - A nock scope (a set of mocked requests scoped to a + * certain base URL). + * @param args.blockNumber - The block number that the block tracker should + * report, as a 0x-prefixed hex string. + */ +async function mockAllBlockTrackerRequests({ + nockScope, + blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, +}: MockBlockTrackerRequestOptions) { + const result = await mockRpcCall({ + nockScope, + request: { method: 'eth_blockNumber', params: [] }, + response: { result: blockNumber }, + }); + + if ('persist' in result) { + result.persist(); + } +} + /** * Makes a JSON-RPC call through the given eth-query object. * - * @param {any} ethQuery - The eth-query object. - * @param {object} request - The request data. - * @returns {Promise} A promise that either resolves with the result from - * the JSON-RPC response if it is successful or rejects with the error from the - * JSON-RPC response otherwise. + * @param ethQuery - The eth-query object. + * @param request - The request data. + * @returns A promise that either resolves with the result from the JSON-RPC + * response if it is successful or rejects with the error from the JSON-RPC + * response otherwise. */ -function makeRpcCall(ethQuery, request) { +function makeRpcCall(ethQuery: EthQuery, request: Request) { return new Promise((resolve, reject) => { debug('[makeRpcCall] making request', request); ethQuery.sendAsync(request, (error, result) => { @@ -227,41 +276,43 @@ function makeRpcCall(ethQuery, request) { }); } -/** - * @typedef {{providerType: 'infura' | 'custom', infuraNetwork?: string}} WithMockedCommunicationsOptions - * - * The options bag that `Communications` takes. - */ +export type ProviderType = 'infura' | 'custom'; -/** - * @typedef {{mockNextBlockTrackerRequest: (options: Omit) => void, mockAllBlockTrackerRequests: (options: Omit) => void, mockRpcCall: (options: Omit) => NockScope, rpcUrl: string, infuraNetwork: string}} Communications - * - * Provides methods to mock different kinds of requests to the provider. - */ +export type MockOptions = { + infuraNetwork?: BuiltInInfuraNetwork; + providerType: ProviderType; + customRpcUrl?: string; + customChainId?: Hex; +}; -/** - * @typedef {(comms: Communications) => Promise} WithMockedCommunicationsCallback - * - * The callback that `mockingCommunications` takes. - */ +export type MockCommunications = { + mockNextBlockTrackerRequest: (options?: any) => void; + mockAllBlockTrackerRequests: (options?: any) => void; + mockRpcCall: (options: CurriedMockRpcCallOptions) => MockRpcCallResult; + rpcUrl: string; + infuraNetwork: BuiltInInfuraNetwork; +}; /** * Sets up request mocks for requests to the provider. * - * @param {WithMockedCommunicationsOptions} options - An options bag. - * @param {"infura" | "custom"} options.providerType - The type of network - * client being tested. - * @param {string} [options.infuraNetwork] - The name of the Infura network being - * tested, assuming that `providerType` is "infura" (default: "mainnet"). - * @param {string} [options.customRpcUrl] - The URL of the custom RPC endpoint, - * assuming that `providerType` is "custom". - * @param {WithMockedCommunicationsCallback} fn - A function which will be - * called with an object that allows interaction with the network client. - * @returns {Promise} The return value of the given function. + * @param options - An options bag. + * @param options.providerType - The type of network client being tested. + * @param options.infuraNetwork - The name of the Infura network being tested, + * assuming that `providerType` is "infura" (default: "mainnet"). + * @param options.customRpcUrl - The URL of the custom RPC endpoint, assuming + * that `providerType` is "custom". + * @param fn - A function which will be called with an object that allows + * interaction with the network client. + * @returns The return value of the given function. */ export async function withMockedCommunications( - { providerType, infuraNetwork = 'mainnet', customRpcUrl = MOCK_RPC_URL }, - fn, + { + providerType, + infuraNetwork = 'mainnet', + customRpcUrl = MOCK_RPC_URL, + }: MockOptions, + fn: (comms: MockCommunications) => Promise, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( @@ -274,11 +325,11 @@ export async function withMockedCommunications( ? `https://${infuraNetwork}.infura.io` : customRpcUrl; const nockScope = buildScopeForMockingRequests(rpcUrl); - const curriedMockNextBlockTrackerRequest = (localOptions) => + const curriedMockNextBlockTrackerRequest = (localOptions: any) => mockNextBlockTrackerRequest({ nockScope, ...localOptions }); - const curriedMockAllBlockTrackerRequests = (localOptions) => + const curriedMockAllBlockTrackerRequests = (localOptions: any) => mockAllBlockTrackerRequests({ nockScope, ...localOptions }); - const curriedMockRpcCall = (localOptions) => + const curriedMockRpcCall = (localOptions: any) => mockRpcCall({ nockScope, ...localOptions }); const comms = { @@ -297,12 +348,12 @@ export async function withMockedCommunications( } } -/** - * @typedef {{blockTracker: import('eth-block-tracker').PollingBlockTracker, clock: sinon.SinonFakeTimers, makeRpcCall: (request: Partial) => Promise, makeRpcCallsInSeries: (requests: Partial[]) => Promise}} MockNetworkClient - * - * Provides methods to interact with the suite of middleware that - * `createInfuraClient` or `createJsonRpcClient` exposes. - */ +type MockNetworkClient = { + blockTracker: any; + clock: sinon.SinonFakeTimers; + makeRpcCall: (request: Request) => Promise; + makeRpcCallsInSeries: (requests: Request[]) => Promise; +}; /** * Some middleware contain logic which retries the request if some condition @@ -321,14 +372,14 @@ export async function withMockedCommunications( * `setTimeout` handler. */ export async function waitForPromiseToBeFulfilledAfterRunningAllTimers( - promise, - clock, + promise: any, + clock: any, ) { let hasPromiseBeenFulfilled = false; let numTimesClockHasBeenAdvanced = 0; promise - .catch((error) => { + .catch((error: any) => { // This is used to silence Node.js warnings about the rejection // being handled asynchronously. The error is handled later when // `promise` is awaited, but we log it here anyway in case it gets @@ -350,36 +401,22 @@ export async function waitForPromiseToBeFulfilledAfterRunningAllTimers( return promise; } -/** - * @typedef {{providerType: "infura" | "custom", infuraNetwork?: string, customRpcUrl?: string, customChainId?: string}} WithClientOptions - * - * The options bag that `withNetworkClient` takes. - */ - -/** - * @typedef {(client: MockNetworkClient) => Promise} WithClientCallback - * - * The callback that `withNetworkClient` takes. - */ - /** * Builds a provider from the middleware (for the provider type) along with a * block tracker, runs the given function with those two things, and then * ensures the block tracker is stopped at the end. * - * @param {WithClientOptions} options - An options bag. - * @param {"infura" | "custom"} options.providerType - The type of network - * client being tested. - * @param {string} [options.infuraNetwork] - The name of the Infura network being - * tested, assuming that `providerType` is "infura" (default: "mainnet"). - * @param {string} [options.customRpcUrl] - The URL of the custom RPC endpoint, - * assuming that `providerType` is "custom". - * @param {string} [options.customChainId] - The chain id belonging to the - * custom RPC endpoint, assuming that `providerType` is "custom" (default: - * "0x1"). - * @param {WithClientCallback} fn - A function which will be called with an - * object that allows interaction with the network client. - * @returns {Promise} The return value of the given function. + * @param options - An options bag. + * @param options.providerType - The type of network client being tested. + * @param options.infuraNetwork - The name of the Infura network being tested, + * assuming that `providerType` is "infura" (default: "mainnet"). + * @param options.customRpcUrl - The URL of the custom RPC endpoint, assuming + * that `providerType` is "custom". + * @param options.customChainId - The chain id belonging to the custom RPC + * endpoint, assuming that `providerType` is "custom" (default: "0x1"). + * @param fn - A function which will be called with an object that allows + * interaction with the network client. + * @returns The return value of the given function. */ export async function withNetworkClient( { @@ -387,8 +424,8 @@ export async function withNetworkClient( infuraNetwork = 'mainnet', customRpcUrl = MOCK_RPC_URL, customChainId = '0x1', - }, - fn, + }: MockOptions, + fn: (client: MockNetworkClient) => Promise, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( @@ -414,20 +451,21 @@ export async function withNetworkClient( ? createNetworkClient({ network: infuraNetwork, infuraProjectId: MOCK_INFURA_PROJECT_ID, - type: 'infura', + type: NetworkClientType.Infura, }) : createNetworkClient({ chainId: customChainId, rpcUrl: customRpcUrl, - type: 'custom', + type: NetworkClientType.Custom, }); process.env.IN_TEST = inTest; const { provider, blockTracker } = clientUnderTest; const ethQuery = new EthQuery(provider); - const curriedMakeRpcCall = (request) => makeRpcCall(ethQuery, request); - const makeRpcCallsInSeries = async (requests) => { + const curriedMakeRpcCall = (request: Request) => + makeRpcCall(ethQuery, request); + const makeRpcCallsInSeries = async (requests: Request[]) => { const responses = []; for (const request of requests) { responses.push(await curriedMakeRpcCall(request)); @@ -451,6 +489,13 @@ export async function withNetworkClient( } } +type BuildMockParamsOptions = { + // The block parameter value to set. + blockParam: any; + // The index of the block parameter. + blockParamIndex: number; +}; + /** * Build mock parameters for a JSON-RPC call. * @@ -460,16 +505,15 @@ export async function withNetworkClient( * The block parameter can be set to a custom value. If no value is given, it * is set as undefined. * - * @param {object} args - Arguments. - * @param {number} args.blockParamIndex - The index of the block parameter. - * @param {any} [args.blockParam] - The block parameter value to set. - * @returns {any[]} The mock params. + * @param args - Arguments. + * @param args.blockParamIndex - The index of the block parameter. + * @param args.blockParam - The block parameter value to set. + * @returns The mock params. */ -export function buildMockParams({ blockParam, blockParamIndex }) { - if (blockParamIndex === undefined) { - throw new Error(`Missing 'blockParamIndex'`); - } - +export function buildMockParams({ + blockParam, + blockParamIndex, +}: BuildMockParamsOptions) { const params = new Array(blockParamIndex).fill('some value'); params[blockParamIndex] = blockParam; @@ -480,18 +524,18 @@ export function buildMockParams({ blockParam, blockParamIndex }) { * Returns a partial JSON-RPC request object, with the "block" param replaced * with the given value. * - * @param {object} request - The request object. - * @param {string} request.method - The request method. - * @param {params} [request.params] - The request params. - * @param {number} blockParamIndex - The index within the `params` array of the - * block param. - * @param {any} blockParam - The desired block param value. - * @returns {object} The updated request object. + * @param request - The request object. + * @param request.method - The request method. + * @param request.params - The request params. + * @param blockParamIndex - The index within the `params` array of the block + * param. + * @param blockParam - The desired block param value. + * @returns The updated request object. */ export function buildRequestWithReplacedBlockParam( - { method, params = [] }, - blockParamIndex, - blockParam, + { method, params = [] }: Request, + blockParamIndex: number, + blockParam: any, ) { const updatedParams = params.slice(); updatedParams[blockParamIndex] = blockParam; diff --git a/app/scripts/controllers/network/provider-api-tests/no-block-param.js b/app/scripts/controllers/network/provider-api-tests/no-block-param.ts similarity index 99% rename from app/scripts/controllers/network/provider-api-tests/no-block-param.js rename to app/scripts/controllers/network/provider-api-tests/no-block-param.ts index 08ae7edd0977..662c7fac92fd 100644 --- a/app/scripts/controllers/network/provider-api-tests/no-block-param.js +++ b/app/scripts/controllers/network/provider-api-tests/no-block-param.ts @@ -1,6 +1,7 @@ /* eslint-disable jest/require-top-level-describe, jest/no-export */ import { + ProviderType, waitForPromiseToBeFulfilledAfterRunningAllTimers, withMockedCommunications, withNetworkClient, @@ -11,6 +12,11 @@ import { buildJsonRpcEngineEmptyResponseErrorMessage, } from './shared-tests'; +type TestsForRpcMethodAssumingNoBlockParamOptions = { + providerType: ProviderType; + numberOfParameters: number; +}; + /** * Defines tests which exercise the behavior exhibited by an RPC method which is * assumed to not take a block parameter. Even if it does, the value of this @@ -23,8 +29,11 @@ import { * either `infura` or `custom` (default: "infura"). */ export function testsForRpcMethodAssumingNoBlockParam( - method, - { numberOfParameters, providerType }, + method: string, + { + numberOfParameters, + providerType, + }: TestsForRpcMethodAssumingNoBlockParamOptions, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( diff --git a/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.js b/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.ts similarity index 83% rename from app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.js rename to app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.ts index 693d9f779eeb..ee92bb07f217 100644 --- a/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.js +++ b/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.ts @@ -1,7 +1,16 @@ /* eslint-disable jest/require-top-level-describe, jest/no-export */ import { fill } from 'lodash'; -import { withMockedCommunications, withNetworkClient } from './helpers'; +import { + ProviderType, + withMockedCommunications, + withNetworkClient, +} from './helpers'; + +type TestsForRpcMethodNotHandledByMiddlewareOptions = { + providerType: ProviderType; + numberOfParameters: number; +}; /** * Defines tests which exercise the behavior exhibited by an RPC method that @@ -15,8 +24,11 @@ import { withMockedCommunications, withNetworkClient } from './helpers'; * RPC method takes. */ export function testsForRpcMethodNotHandledByMiddleware( - method, - { providerType, numberOfParameters }, + method: string, + { + providerType, + numberOfParameters, + }: TestsForRpcMethodNotHandledByMiddlewareOptions, ) { if (providerType !== 'infura' && providerType !== 'custom') { throw new Error( diff --git a/app/scripts/controllers/network/provider-api-tests/shared-tests.js b/app/scripts/controllers/network/provider-api-tests/shared-tests.ts similarity index 98% rename from app/scripts/controllers/network/provider-api-tests/shared-tests.js rename to app/scripts/controllers/network/provider-api-tests/shared-tests.ts index 04412d3f0841..6337bb56a789 100644 --- a/app/scripts/controllers/network/provider-api-tests/shared-tests.js +++ b/app/scripts/controllers/network/provider-api-tests/shared-tests.ts @@ -2,7 +2,11 @@ import { testsForRpcMethodsThatCheckForBlockHashInResponse } from './block-hash-in-response'; import { testsForRpcMethodSupportingBlockParam } from './block-param'; -import { withMockedCommunications, withNetworkClient } from './helpers'; +import { + ProviderType, + withMockedCommunications, + withNetworkClient, +} from './helpers'; import { testsForRpcMethodAssumingNoBlockParam } from './no-block-param'; import { testsForRpcMethodNotHandledByMiddleware } from './not-handled-by-middleware'; @@ -13,7 +17,7 @@ import { testsForRpcMethodNotHandledByMiddleware } from './not-handled-by-middle * @param reason - The exact reason for failure. * @returns The error message. */ -export function buildInfuraClientRetriesExhaustedErrorMessage(reason) { +export function buildInfuraClientRetriesExhaustedErrorMessage(reason: string) { return new RegExp( `^InfuraProvider - cannot complete request. All retries exhausted\\..+${reason}`, 'us', @@ -27,7 +31,7 @@ export function buildInfuraClientRetriesExhaustedErrorMessage(reason) { * @param method - The RPC method. * @returns The error message. */ -export function buildJsonRpcEngineEmptyResponseErrorMessage(method) { +export function buildJsonRpcEngineEmptyResponseErrorMessage(method: string) { return new RegExp( `^JsonRpcEngine: Response has no error or result for request:.+"method": "${method}"`, 'us', @@ -42,7 +46,7 @@ export function buildJsonRpcEngineEmptyResponseErrorMessage(method) { * @param reason - The reason. * @returns The error message. */ -export function buildFetchFailedErrorMessage(url, reason) { +export function buildFetchFailedErrorMessage(url: string, reason: string) { return new RegExp( `^request to ${url}(/[^/ ]*)+ failed, reason: ${reason}`, 'us', @@ -57,7 +61,7 @@ export function buildFetchFailedErrorMessage(url, reason) { * exposed by `createInfuraClient` is tested; if `custom`, then the middleware * exposed by `createJsonRpcClient` will be tested. */ -export function testsForProviderType(providerType) { +export function testsForProviderType(providerType: ProviderType) { // Ethereum JSON-RPC spec: // Infura documentation: diff --git a/app/scripts/controllers/permissions/flask/snap-permissions.test.js b/app/scripts/controllers/permissions/flask/snap-permissions.test.js index a6ee60f78adb..8c53313ffde5 100644 --- a/app/scripts/controllers/permissions/flask/snap-permissions.test.js +++ b/app/scripts/controllers/permissions/flask/snap-permissions.test.js @@ -16,7 +16,6 @@ describe('buildSnapRestrictedMethodSpecifications', () => { getSnap: () => undefined, getSnapRpcHandler: () => undefined, getSnapState: () => undefined, - showConfirmation: () => undefined, updateSnapState: () => undefined, }; diff --git a/app/scripts/controllers/sign.test.ts b/app/scripts/controllers/sign.test.ts index 36dddf65bf37..dac749466cc7 100644 --- a/app/scripts/controllers/sign.test.ts +++ b/app/scripts/controllers/sign.test.ts @@ -409,6 +409,14 @@ describe('SignController', () => { ); }); + it('does not throw if accepting approval throws', async () => { + messengerMock.call.mockImplementation(() => { + throw new Error('Test Error'); + }); + + await signController[signMethodName](messageParamsMock); + }); + it('rejects message on error', async () => { keyringControllerMock[signMethodName].mockReset(); keyringControllerMock[signMethodName].mockRejectedValue( @@ -468,6 +476,14 @@ describe('SignController', () => { 'Cancel', ); }); + + it('does not throw if rejecting approval throws', async () => { + messengerMock.call.mockImplementation(() => { + throw new Error('Test Error'); + }); + + await signController[cancelMethodName](messageParamsMock); + }); }); describe('message manager events', () => { diff --git a/app/scripts/controllers/sign.ts b/app/scripts/controllers/sign.ts index 1712ed1ee1dd..d27de2f0605d 100644 --- a/app/scripts/controllers/sign.ts +++ b/app/scripts/controllers/sign.ts @@ -33,12 +33,13 @@ import { RejectRequest, } from '@metamask/approval-controller'; import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; +import { MESSAGE_TYPE } from '../../../shared/constants/app'; import PreferencesController from './preferences'; const controllerName = 'SignController'; -const methodNameSign = 'eth_sign'; -const methodNamePersonalSign = 'personal_sign'; -const methodNameTypedSign = 'eth_signTypedData'; +const methodNameSign = MESSAGE_TYPE.ETH_SIGN; +const methodNamePersonalSign = MESSAGE_TYPE.PERSONAL_SIGN; +const methodNameTypedSign = MESSAGE_TYPE.ETH_SIGN_TYPED_DATA; const stateMetadata = { unapprovedMsgs: { persist: false, anonymous: false }, @@ -636,14 +637,22 @@ export default class SignController extends BaseControllerV2< } private _acceptApproval(messageId: string) { - this.messagingSystem.call('ApprovalController:acceptRequest', messageId); + try { + this.messagingSystem.call('ApprovalController:acceptRequest', messageId); + } catch (error) { + log.info('Failed to accept signature approval request', error); + } } private _rejectApproval(messageId: string) { - this.messagingSystem.call( - 'ApprovalController:rejectRequest', - messageId, - 'Cancel', - ); + try { + this.messagingSystem.call( + 'ApprovalController:rejectRequest', + messageId, + 'Cancel', + ); + } catch (error) { + log.info('Failed to reject signature approval request', error); + } } } diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index c0fc09e24e67..d34404beb4be 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -1,5 +1,7 @@ import { errorCodes } from 'eth-rpc-errors'; import { detectSIWE } from '@metamask/controller-utils'; +import { isValidAddress } from 'ethereumjs-util'; + import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app'; import { TransactionStatus } from '../../../shared/constants/transaction'; import { SECOND } from '../../../shared/constants/time'; @@ -168,8 +170,17 @@ export default function createRPCMethodTrackingMiddleware({ if (event === MetaMetricsEventName.SignatureRequested) { eventProperties.signature_type = method; - const data = req?.params?.[0]; - const from = req?.params?.[1]; + // In personal messages the first param is data while in typed messages second param is data + // if condition below is added to ensure that the right params are captured as data and address. + let data; + let from; + if (isValidAddress(req?.params?.[1])) { + data = req?.params?.[0]; + from = req?.params?.[1]; + } else { + data = req?.params?.[1]; + from = req?.params?.[0]; + } const paramsExamplePassword = req?.params?.[2]; const msgData = { diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index 070ad12b2ac8..5da8206fa763 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -383,5 +383,66 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); }); + + describe('when signature requests are received', () => { + let securityProviderReq, fnHandler; + beforeEach(() => { + securityProviderReq = jest.fn().mockReturnValue(() => + Promise.resolve({ + flagAsDangerous: 0, + }), + ); + + fnHandler = createRPCMethodTrackingMiddleware({ + trackEvent, + getMetricsState, + rateLimitSeconds: 1, + securityProviderRequest: securityProviderReq, + }); + }); + it(`should pass correct data for personal sign`, async () => { + const req = { + method: 'personal_sign', + params: [ + '0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765', + '0x8eeee1781fd885ff5ddef7789486676961873d12', + 'Example password', + ], + jsonrpc: '2.0', + id: 1142196570, + origin: 'https://metamask.github.io', + tabId: 1048582817, + }; + const res = { id: 1142196570, jsonrpc: '2.0' }; + const { next } = getNext(); + + await fnHandler(req, res, next); + + expect(securityProviderReq).toHaveBeenCalledTimes(1); + const call = securityProviderReq.mock.calls[0][0]; + expect(call.msgParams.data).toStrictEqual(req.params[0]); + }); + it(`should pass correct data for typed sign`, async () => { + const req = { + method: 'eth_signTypedData_v4', + params: [ + '0x8eeee1781fd885ff5ddef7789486676961873d12', + '{"domain":{"chainId":"5","name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"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[]"}]}}', + ], + jsonrpc: '2.0', + id: 1142196571, + origin: 'https://metamask.github.io', + tabId: 1048582817, + }; + const res = { id: 1142196571, jsonrpc: '2.0' }; + const { next } = getNext(); + + await fnHandler(req, res, next); + + expect(securityProviderReq).toHaveBeenCalledTimes(1); + const call = securityProviderReq.mock.calls[0][0]; + expect(call.msgParams.data).toStrictEqual(req.params[1]); + }); + }); }); }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 133f5a877cd8..13d986815bb6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -97,8 +97,8 @@ import { import { MILLISECOND, SECOND } from '../../shared/constants/time'; import { ORIGIN_METAMASK, - ///: BEGIN:ONLY_INCLUDE_IN(flask) MESSAGE_TYPE, + ///: BEGIN:ONLY_INCLUDE_IN(flask) SNAP_DIALOG_TYPES, ///: END:ONLY_INCLUDE_IN POLLING_TOKEN_ENVIRONMENT_TYPES, @@ -260,6 +260,11 @@ export default class MetamaskController extends EventEmitter { name: 'ApprovalController', }), showApprovalRequest: opts.showUserConfirmation, + typesExcludedFromRateLimiting: [ + MESSAGE_TYPE.ETH_SIGN, + MESSAGE_TYPE.PERSONAL_SIGN, + MESSAGE_TYPE.ETH_SIGN_TYPED_DATA, + ], }); const networkControllerMessenger = this.controllerMessenger.getRestricted({ @@ -520,9 +525,15 @@ export default class MetamaskController extends EventEmitter { isUnlocked: this.isUnlocked.bind(this), initState: initState.AppStateController, onInactiveTimeout: () => this.setLocked(), - showUnlockRequest: opts.showUserConfirmation, preferencesStore: this.preferencesController.store, qrHardwareStore: this.qrHardwareKeyring.getMemStore(), + messenger: this.controllerMessenger.getRestricted({ + name: 'AppStateController', + allowedActions: [ + `${this.approvalController.name}:addRequest`, + `${this.approvalController.name}:acceptRequest`, + ], + }), }); const currencyRateMessenger = this.controllerMessenger.getRestricted({ @@ -1155,6 +1166,9 @@ export default class MetamaskController extends EventEmitter { preferencesController: this.preferencesController, getState: this.getState.bind(this), securityProviderRequest: this.securityProviderRequest.bind(this), + metricsEvent: this.metaMetricsController.trackEvent.bind( + this.metaMetricsController, + ), }); this.swapsController = new SwapsController({ @@ -1513,12 +1527,6 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:getSnapState', ), - showConfirmation: (origin, confirmationData) => - this.approvalController.addAndShowApprovalRequest({ - origin, - type: MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION, - requestData: confirmationData, - }), showDialog: (origin, type, content, placeholder) => this.approvalController.addAndShowApprovalRequest({ origin, @@ -2026,6 +2034,8 @@ export default class MetamaskController extends EventEmitter { appStateController.setRecoveryPhraseReminderLastShown.bind( appStateController, ), + setTermsOfUseLastAgreed: + appStateController.setTermsOfUseLastAgreed.bind(appStateController), setOutdatedBrowserWarningLastShown: appStateController.setOutdatedBrowserWarningLastShown.bind( appStateController, @@ -3224,32 +3234,6 @@ export default class MetamaskController extends EventEmitter { return await this.txController.newUnapprovedTransaction(txParams, req); } - ///: BEGIN:ONLY_INCLUDE_IN(flask) - /** - * Gets an "app key" corresponding to an Ethereum address. An app key is more - * or less an addrdess hashed together with some string, in this case a - * subject identifier / origin. - * - * @todo Figure out a way to derive app keys that doesn't depend on the user's - * Ethereum addresses. - * @param {string} subject - The identifier of the subject whose app key to - * retrieve. - * @param {string} [requestedAccount] - The account whose app key to retrieve. - * The first account in the keyring will be used by default. - */ - async getAppKeyForSubject(subject, requestedAccount) { - let account; - - if (requestedAccount) { - account = requestedAccount; - } else { - [account] = await this.keyringController.getAccounts(); - } - - return this.keyringController.exportAppKeyForAddress(account, subject); - } - ///: END:ONLY_INCLUDE_IN - // eth_decrypt methods /** @@ -3855,7 +3839,6 @@ export default class MetamaskController extends EventEmitter { ///: BEGIN:ONLY_INCLUDE_IN(flask) engine.push( createSnapMethodMiddleware(subjectType === SubjectType.Snap, { - getAppKey: this.getAppKeyForSubject.bind(this, origin), getUnlockPromise: this.appStateController.getUnlockPromise.bind( this.appStateController, ), diff --git a/app/scripts/migrations/085.test.js b/app/scripts/migrations/085.test.js new file mode 100644 index 000000000000..6b7b4967d40a --- /dev/null +++ b/app/scripts/migrations/085.test.js @@ -0,0 +1,91 @@ +import { migrate, version } from './085'; + +jest.mock('uuid', () => { + const actual = jest.requireActual('uuid'); + + return { + ...actual, + v4: jest.fn(), + }; +}); + +describe('migration #85', () => { + it('should update the version metadata', async () => { + const oldStorage = { + meta: { + version: 84, + }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ + version, + }); + }); + + it('should return state unaltered if there is no network controller state', async () => { + const oldData = { + other: 'data', + }; + const oldStorage = { + meta: { + version: 84, + }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldData); + }); + + it('should return state unaltered if there is no network controller previous provider state', async () => { + const oldData = { + other: 'data', + NetworkController: { + provider: { + some: 'provider', + }, + }, + }; + const oldStorage = { + meta: { + version: 84, + }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual(oldData); + }); + + it('should remove the previous provider state', async () => { + const oldData = { + other: 'data', + NetworkController: { + previousProviderStore: { + example: 'config', + }, + provider: { + some: 'provider', + }, + }, + }; + const oldStorage = { + meta: { + version: 84, + }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data).toStrictEqual({ + other: 'data', + NetworkController: { + provider: { + some: 'provider', + }, + }, + }); + }); +}); diff --git a/app/scripts/migrations/085.ts b/app/scripts/migrations/085.ts new file mode 100644 index 000000000000..03499d2b2c49 --- /dev/null +++ b/app/scripts/migrations/085.ts @@ -0,0 +1,33 @@ +import { cloneDeep } from 'lodash'; +import { isObject } from '@metamask/utils'; + +export const version = 85; + +/** + * Remove the now-obsolete network controller `previousProviderStore` state. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate(originalVersionedData: { + meta: { version: number }; + data: Record; +}) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + versionedData.data = transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + if (!isObject(state.NetworkController)) { + return state; + } + + delete state.NetworkController.previousProviderStore; + + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 54a09c2b4e10..5cbe4ee04a56 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -88,6 +88,7 @@ import * as m081 from './081'; import * as m082 from './082'; import * as m083 from './083'; import * as m084 from './084'; +import * as m085 from './085'; const migrations = [ m002, @@ -173,6 +174,7 @@ const migrations = [ m082, m083, m084, + m085, ]; export default migrations; diff --git a/coverage-targets.js b/coverage-targets.js index 6097331d1d72..1ed939c2fc45 100644 --- a/coverage-targets.js +++ b/coverage-targets.js @@ -6,10 +6,10 @@ // subset of files to check against these targets. module.exports = { global: { - lines: 66, - branches: 54.4, - statements: 65, - functions: 58.5, + lines: 67.8, + branches: 55.84, + statements: 67.13, + functions: 59.66, }, transforms: { branches: 100, diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index e594e00f636b..6f5312b9d5c3 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1054,7 +1054,7 @@ "@metamask/eth-token-tracker>deep-equal>is-date-object": true, "@ngraveio/bc-ur>assert>object-is": true, "@storybook/api>telejson>is-regex": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>regexp.prototype.flags": true } }, @@ -2484,7 +2484,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>os-browserify": { @@ -3539,7 +3539,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -4159,7 +4159,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -4171,7 +4171,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index 16b051712c6f..ac604bd4db7f 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -1126,7 +1126,7 @@ "@metamask/eth-token-tracker>deep-equal>is-date-object": true, "@ngraveio/bc-ur>assert>object-is": true, "@storybook/api>telejson>is-regex": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>regexp.prototype.flags": true } }, @@ -2877,7 +2877,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>os-browserify": { @@ -3932,7 +3932,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -4346,9 +4346,9 @@ "react-markdown>unified": { "packages": { "jsdom>request>extend": true, + "mocha>yargs-unparser>is-plain-obj": true, "react-markdown>unified>bail": true, "react-markdown>unified>is-buffer": true, - "react-markdown>unified>is-plain-obj": true, "react-markdown>unified>trough": true, "react-markdown>vfile": true } @@ -4684,7 +4684,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -4696,7 +4696,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 16b051712c6f..ac604bd4db7f 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1126,7 +1126,7 @@ "@metamask/eth-token-tracker>deep-equal>is-date-object": true, "@ngraveio/bc-ur>assert>object-is": true, "@storybook/api>telejson>is-regex": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>regexp.prototype.flags": true } }, @@ -2877,7 +2877,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>os-browserify": { @@ -3932,7 +3932,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -4346,9 +4346,9 @@ "react-markdown>unified": { "packages": { "jsdom>request>extend": true, + "mocha>yargs-unparser>is-plain-obj": true, "react-markdown>unified>bail": true, "react-markdown>unified>is-buffer": true, - "react-markdown>unified>is-plain-obj": true, "react-markdown>unified>trough": true, "react-markdown>vfile": true } @@ -4684,7 +4684,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -4696,7 +4696,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index e594e00f636b..6f5312b9d5c3 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1054,7 +1054,7 @@ "@metamask/eth-token-tracker>deep-equal>is-date-object": true, "@ngraveio/bc-ur>assert>object-is": true, "@storybook/api>telejson>is-regex": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>regexp.prototype.flags": true } }, @@ -2484,7 +2484,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>os-browserify": { @@ -3539,7 +3539,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -4159,7 +4159,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -4171,7 +4171,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 871dcc17288c..b3d9ce0d5b21 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1697,7 +1697,7 @@ }, "browserify>has": { "packages": { - "mocha>object.assign>function-bind": true + "browserify>has>function-bind": true } }, "browserify>insert-module-globals": { @@ -1885,12 +1885,7 @@ "process.platform": true }, "packages": { - "chalk>supports-color>has-flag": true - } - }, - "chalk>supports-color>has-flag": { - "globals": { - "process.argv": true + "sinon>supports-color>has-flag": true } }, "chokidar": { @@ -2002,7 +1997,7 @@ "packages": { "cross-spawn>path-key": true, "cross-spawn>shebang-command": true, - "cross-spawn>which": true + "mocha>which": true } }, "cross-spawn>path-key": { @@ -2016,21 +2011,6 @@ "cross-spawn>shebang-command>shebang-regex": true } }, - "cross-spawn>which": { - "builtin": { - "path.join": true - }, - "globals": { - "process.cwd": true, - "process.env.OSTYPE": true, - "process.env.PATH": true, - "process.env.PATHEXT": true, - "process.platform": true - }, - "packages": { - "mocha>which>isexe": true - } - }, "debounce-stream>duplexer": { "builtin": { "stream": true @@ -2350,7 +2330,7 @@ "process": true }, "packages": { - "gulp-livereload>debug>ms": true, + "mocha>ms": true, "mocha>supports-color": true } }, @@ -2471,7 +2451,7 @@ "process": true }, "packages": { - "gulp-livereload>debug>ms": true, + "mocha>ms": true, "mocha>supports-color": true } }, @@ -2871,8 +2851,8 @@ "eslint>@eslint/eslintrc>globals": true, "eslint>ajv": true, "eslint>minimatch": true, - "eslint>strip-json-comments": true, "globby>ignore": true, + "mocha>strip-json-comments": true, "nock>debug": true } }, @@ -3094,9 +3074,9 @@ "process.cwd": true }, "packages": { - "chokidar>glob-parent": true, "fast-glob>@nodelib/fs.stat": true, "fast-glob>@nodelib/fs.walk": true, + "fast-glob>glob-parent": true, "globby>merge2": true, "stylelint>micromatch": true } @@ -3151,6 +3131,15 @@ "fast-glob>@nodelib/fs.walk>fastq>reusify": true } }, + "fast-glob>glob-parent": { + "builtin": { + "os.platform": true, + "path.posix.dirname": true + }, + "packages": { + "eslint>is-glob": true + } + }, "fs-extra": { "builtin": { "assert": true, @@ -3200,7 +3189,7 @@ "globalthis>define-properties": { "packages": { "globalthis>define-properties>has-property-descriptors": true, - "mocha>object.assign>object-keys": true + "globalthis>define-properties>object-keys": true } }, "globalthis>define-properties>has-property-descriptors": { @@ -3370,8 +3359,8 @@ }, "packages": { "gulp-dart-sass>chalk>ansi-styles": true, - "gulp-dart-sass>chalk>supports-color": true, - "mocha>escape-string-regexp": true + "gulp-dart-sass>chalk>escape-string-regexp": true, + "gulp-dart-sass>chalk>supports-color": true } }, "gulp-dart-sass>chalk>ansi-styles": { @@ -3391,7 +3380,12 @@ "process.versions.node.split": true }, "packages": { - "mocha>supports-color>has-flag": true + "gulp-dart-sass>chalk>supports-color>has-flag": true + } + }, + "gulp-dart-sass>chalk>supports-color>has-flag": { + "globals": { + "process.argv": true } }, "gulp-dart-sass>strip-ansi": { @@ -3430,9 +3424,9 @@ "process.platform": true }, "packages": { + "gulp-dart-sass>chalk>escape-string-regexp": true, "gulp-livereload>chalk>ansi-styles": true, - "gulp-livereload>chalk>supports-color": true, - "mocha>escape-string-regexp": true + "gulp-livereload>chalk>supports-color": true } }, "gulp-livereload>chalk>ansi-styles": { @@ -3452,7 +3446,12 @@ "process.versions.node.split": true }, "packages": { - "mocha>supports-color>has-flag": true + "gulp-livereload>chalk>supports-color>has-flag": true + } + }, + "gulp-livereload>chalk>supports-color>has-flag": { + "globals": { + "process.argv": true } }, "gulp-livereload>debug": { @@ -3469,7 +3468,7 @@ }, "packages": { "gulp-livereload>chalk>supports-color": true, - "gulp-livereload>debug>ms": true + "mocha>ms": true } }, "gulp-livereload>event-stream": { @@ -3595,7 +3594,7 @@ "process": true }, "packages": { - "gulp-livereload>debug>ms": true, + "mocha>ms": true, "mocha>supports-color": true } }, @@ -3699,9 +3698,9 @@ "process.platform": true }, "packages": { + "gulp-dart-sass>chalk>escape-string-regexp": true, "gulp-rtlcss>rtlcss>chalk>ansi-styles": true, - "gulp-rtlcss>rtlcss>chalk>supports-color": true, - "mocha>escape-string-regexp": true + "gulp-rtlcss>rtlcss>chalk>supports-color": true } }, "gulp-rtlcss>rtlcss>chalk>ansi-styles": { @@ -3721,7 +3720,12 @@ "process.versions.node.split": true }, "packages": { - "mocha>supports-color>has-flag": true + "gulp-rtlcss>rtlcss>chalk>supports-color>has-flag": true + } + }, + "gulp-rtlcss>rtlcss>chalk>supports-color>has-flag": { + "globals": { + "process.argv": true } }, "gulp-rtlcss>rtlcss>postcss": { @@ -3947,7 +3951,7 @@ "process": true }, "packages": { - "gulp-livereload>debug>ms": true, + "mocha>ms": true, "mocha>supports-color": true } }, @@ -5204,11 +5208,11 @@ "chokidar>normalize-path": true, "eslint>is-glob": true, "gulp-watch>chokidar>async-each": true, + "gulp-watch>glob-parent": true, "gulp-watch>path-is-absolute": true, "gulp>glob-watcher>anymatch": true, "gulp>glob-watcher>chokidar>braces": true, "gulp>glob-watcher>chokidar>fsevents": true, - "gulp>glob-watcher>chokidar>glob-parent": true, "gulp>glob-watcher>chokidar>is-binary-path": true, "gulp>glob-watcher>chokidar>readdirp": true, "gulp>glob-watcher>chokidar>upath": true, @@ -5274,21 +5278,6 @@ "gulp-watch>chokidar>fsevents>node-pre-gyp": true } }, - "gulp>glob-watcher>chokidar>glob-parent": { - "builtin": { - "os.platform": true, - "path": true - }, - "packages": { - "gulp-watch>glob-parent>path-dirname": true, - "gulp>glob-watcher>chokidar>glob-parent>is-glob": true - } - }, - "gulp>glob-watcher>chokidar>glob-parent>is-glob": { - "packages": { - "gulp>glob-watcher>chokidar>glob-parent>is-glob>is-extglob": true - } - }, "gulp>glob-watcher>chokidar>is-binary-path": { "builtin": { "path.extname": true @@ -5663,8 +5652,8 @@ "process.nextTick": true }, "packages": { + "gulp-watch>glob-parent": true, "gulp>glob-watcher>is-negated-glob": true, - "gulp>vinyl-fs>glob-stream>glob-parent": true, "gulp>vinyl-fs>glob-stream>ordered-read-streams": true, "gulp>vinyl-fs>glob-stream>pumpify": true, "gulp>vinyl-fs>glob-stream>to-absolute-glob": true, @@ -5675,21 +5664,6 @@ "vinyl>remove-trailing-separator": true } }, - "gulp>vinyl-fs>glob-stream>glob-parent": { - "builtin": { - "os.platform": true, - "path": true - }, - "packages": { - "gulp-watch>glob-parent>path-dirname": true, - "gulp>vinyl-fs>glob-stream>glob-parent>is-glob": true - } - }, - "gulp>vinyl-fs>glob-stream>glob-parent>is-glob": { - "packages": { - "gulp>vinyl-fs>glob-stream>glob-parent>is-glob>is-extglob": true - } - }, "gulp>vinyl-fs>glob-stream>ordered-read-streams": { "builtin": { "util.inherits": true @@ -5791,7 +5765,7 @@ "gulp>vinyl-fs>object.assign": { "packages": { "globalthis>define-properties": true, - "mocha>object.assign>object-keys": true, + "globalthis>define-properties>object-keys": true, "string.prototype.matchall>call-bind": true, "string.prototype.matchall>has-symbols": true } @@ -6092,9 +6066,9 @@ "process.platform": true }, "packages": { + "gulp-dart-sass>chalk>escape-string-regexp": true, "lavamoat>@babel/highlight>chalk>ansi-styles": true, - "lavamoat>@babel/highlight>chalk>supports-color": true, - "mocha>escape-string-regexp": true + "lavamoat>@babel/highlight>chalk>supports-color": true } }, "lavamoat>@babel/highlight>chalk>ansi-styles": { @@ -6114,7 +6088,12 @@ "process.versions.node.split": true }, "packages": { - "mocha>supports-color>has-flag": true + "lavamoat>@babel/highlight>chalk>supports-color>has-flag": true + } + }, + "lavamoat>@babel/highlight>chalk>supports-color>has-flag": { + "globals": { + "process.argv": true } }, "lavamoat>@lavamoat/aa": { @@ -6272,6 +6251,31 @@ "process.platform": true } }, + "mocha>log-symbols": { + "packages": { + "madge>ora>is-unicode-supported": true, + "mocha>log-symbols>chalk": true + } + }, + "mocha>log-symbols>chalk": { + "packages": { + "chalk>ansi-styles": true, + "mocha>log-symbols>chalk>supports-color": true + } + }, + "mocha>log-symbols>chalk>supports-color": { + "builtin": { + "os.release": true, + "tty.isatty": true + }, + "globals": { + "process.env": true, + "process.platform": true + }, + "packages": { + "sinon>supports-color>has-flag": true + } + }, "mocha>minimatch>brace-expansion": { "packages": { "mocha>minimatch>brace-expansion>concat-map": true, @@ -6280,22 +6284,15 @@ }, "mocha>supports-color": { "builtin": { - "os.release": true + "os.release": true, + "tty.isatty": true }, "globals": { "process.env": true, - "process.platform": true, - "process.stderr": true, - "process.stdout": true, - "process.versions.node.split": true + "process.platform": true }, "packages": { - "mocha>supports-color>has-flag": true - } - }, - "mocha>supports-color>has-flag": { - "globals": { - "process.argv": true + "sinon>supports-color>has-flag": true } }, "mocha>which": { @@ -6565,9 +6562,9 @@ "react-markdown>unified": { "packages": { "jsdom>request>extend": true, + "mocha>yargs-unparser>is-plain-obj": true, "react-markdown>unified>bail": true, "react-markdown>unified>is-buffer": true, - "react-markdown>unified>is-plain-obj": true, "react-markdown>unified>trough": true, "react-markdown>vfile": true } @@ -6767,6 +6764,11 @@ "process.platform": true } }, + "sinon>supports-color>has-flag": { + "globals": { + "process.argv": true + } + }, "source-map": { "builtin": { "fs.readFile": true, @@ -6801,7 +6803,7 @@ }, "string.prototype.matchall>call-bind": { "packages": { - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>get-intrinsic": true } }, @@ -6840,7 +6842,7 @@ }, "packages": { "browserify>has": true, - "mocha>object.assign>function-bind": true, + "browserify>has>function-bind": true, "string.prototype.matchall>has-symbols": true } }, @@ -6896,6 +6898,7 @@ "globby>ignore": true, "globby>slash": true, "lodash": true, + "mocha>log-symbols": true, "nock>debug": true, "nyc>resolve-from": true, "stylelint>@stylelint/postcss-css-in-js": true, @@ -6912,7 +6915,6 @@ "stylelint>import-lazy": true, "stylelint>known-css-properties": true, "stylelint>leven": true, - "stylelint>log-symbols": true, "stylelint>mathml-tag-names": true, "stylelint>micromatch": true, "stylelint>normalize-selector": true, @@ -7094,12 +7096,7 @@ "process.platform": true }, "packages": { - "stylelint>chalk>supports-color>has-flag": true - } - }, - "stylelint>chalk>supports-color>has-flag": { - "globals": { - "process.argv": true + "sinon>supports-color>has-flag": true } }, "stylelint>cosmiconfig": { @@ -7212,8 +7209,8 @@ "process.platform": true }, "packages": { - "mocha>which": true, - "stylelint>global-modules>global-prefix>ini": true + "stylelint>global-modules>global-prefix>ini": true, + "stylelint>global-modules>global-prefix>which": true } }, "stylelint>global-modules>global-prefix>ini": { @@ -7221,39 +7218,24 @@ "process": true } }, - "stylelint>globjoin": { + "stylelint>global-modules>global-prefix>which": { "builtin": { "path.join": true - } - }, - "stylelint>log-symbols": { - "packages": { - "madge>ora>is-unicode-supported": true, - "stylelint>log-symbols>chalk": true - } - }, - "stylelint>log-symbols>chalk": { - "packages": { - "chalk>ansi-styles": true, - "stylelint>log-symbols>chalk>supports-color": true - } - }, - "stylelint>log-symbols>chalk>supports-color": { - "builtin": { - "os.release": true, - "tty.isatty": true }, "globals": { - "process.env": true, + "process.cwd": true, + "process.env.OSTYPE": true, + "process.env.PATH": true, + "process.env.PATHEXT": true, "process.platform": true }, "packages": { - "stylelint>log-symbols>chalk>supports-color>has-flag": true + "mocha>which>isexe": true } }, - "stylelint>log-symbols>chalk>supports-color>has-flag": { - "globals": { - "process.argv": true + "stylelint>globjoin": { + "builtin": { + "path.join": true } }, "stylelint>micromatch": { @@ -7557,9 +7539,9 @@ }, "stylelint>table>slice-ansi": { "packages": { - "mocha>yargs>string-width>is-fullwidth-code-point": true, "stylelint>table>slice-ansi>ansi-styles": true, - "stylelint>table>slice-ansi>astral-regex": true + "stylelint>table>slice-ansi>astral-regex": true, + "stylelint>table>slice-ansi>is-fullwidth-code-point": true } }, "stylelint>table>slice-ansi>ansi-styles": { @@ -7569,7 +7551,7 @@ }, "stylelint>table>string-width": { "packages": { - "mocha>yargs>string-width>is-fullwidth-code-point": true, + "stylelint>table>slice-ansi>is-fullwidth-code-point": true, "stylelint>table>string-width>emoji-regex": true, "stylelint>table>string-width>strip-ansi": true } diff --git a/package.json b/package.json index ac58eca27183..f0c815146764 100644 --- a/package.json +++ b/package.json @@ -228,7 +228,7 @@ "@material-ui/core": "^4.11.0", "@metamask/address-book-controller": "^2.0.0", "@metamask/announcement-controller": "^3.0.0", - "@metamask/approval-controller": "^2.0.0", + "@metamask/approval-controller": "^2.1.0", "@metamask/assets-controllers": "^5.0.0", "@metamask/base-controller": "^2.0.0", "@metamask/contract-metadata": "^2.3.1", @@ -368,6 +368,7 @@ "@babel/preset-typescript": "^7.16.7", "@babel/register": "^7.5.5", "@ethersproject/bignumber": "^5.7.0", + "@json-rpc-specification/meta-schema": "^1.0.6", "@lavamoat/allow-scripts": "^2.0.3", "@lavamoat/lavapack": "^5.0.0", "@metamask/auto-changelog": "^2.1.0", @@ -452,7 +453,7 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^26.6.0", "eslint-plugin-jsdoc": "^39.3.3", - "eslint-plugin-mocha": "^8.1.0", + "eslint-plugin-mocha": "^10.1.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.23.1", @@ -496,7 +497,7 @@ "lockfile-lint": "^4.9.6", "loose-envify": "^1.4.0", "madge": "^5.0.1", - "mocha": "^7.2.0", + "mocha": "^9.2.2", "mockttp": "^2.6.0", "nock": "^13.2.9", "node-fetch": "^2.6.1", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 03626ff61389..44647668b93a 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -510,6 +510,8 @@ export enum MetaMetricsEventName { SignatureFailed = 'Signature Failed', SignatureRejected = 'Signature Rejected', SignatureRequested = 'Signature Requested', + TermsOfUseShown = 'Terms of Use Shown', + TermsOfUseAccepted = 'Terms of Use Accepted', TokenImportButtonClicked = 'Import Token Button Clicked', TokenScreenOpened = 'Token Screen Opened', SupportLinkClicked = 'Support Link Clicked', diff --git a/shared/constants/terms.js b/shared/constants/terms.js new file mode 100644 index 000000000000..5ced85fb529b --- /dev/null +++ b/shared/constants/terms.js @@ -0,0 +1 @@ +export const TERMS_OF_USE_LAST_UPDATED = '2023-03-25'; diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 6e460ef8ef54..a8d379dcdc8e 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -2,6 +2,13 @@ "DNS": { "resolution": "" }, + "activeTab": { + "id": 113, + "title": "E2E Test Dapp", + "origin": "https://metamask.github.io", + "protocol": "https:", + "url": "https://metamask.github.io/test-dapp/" + }, "appState": { "networkDropdownOpen": false, "gasIsLoading": false, @@ -16,7 +23,8 @@ "name": null } }, - "warning": null + "warning": null, + "alertOpen": false }, "confirmTransaction": { "txData": { @@ -42,6 +50,10 @@ "history": { "mostRecentOverviewPage": "/mostRecentOverviewPage" }, + "invalidCustomNetwork": { + "state": "CLOSED", + "networkName": "" + }, "metamask": { "ipfsGateway": "", "dismissSeedBackUpReminder": false, @@ -88,6 +100,14 @@ "ensResolutionsByAddress": {}, "isAccountMenuOpen": false, "isUnlocked": true, + "completedOnboarding": true, + "usedNetworks": { + "0x1": true, + "0x5": true, + "0x539": true + }, + "showTestnetMessageInDropdown": true, + "networkConfigurations": {}, "alertEnabledness": { "unconnectedAccount": true }, @@ -1358,5 +1378,8 @@ "balance": "0x4563918244f40000" }, "stage": "DRAFT" + }, + "unconnectedAccount": { + "state": "CLOSED" } } diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 3ded4081b6cc..aeecb13886c8 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -140,6 +140,7 @@ function defaultFixture() { browserEnvironment: {}, nftsDropdownState: {}, connectedStatusPopoverHasBeenShown: true, + termsOfUseLastAgreed: 86400000000000, defaultHomeActiveTabName: null, fullScreenGasPollTokens: [], notificationGasPollTokens: [], diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 2569dd4a33ed..132003451ceb 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -222,6 +222,9 @@ const getWindowHandles = async (driver, handlesCount) => { }; const importSRPOnboardingFlow = async (driver, seedPhrase, password) => { + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-import-wallet"]'); @@ -262,6 +265,9 @@ const completeImportSRPOnboardingFlowWordByWord = async ( seedPhrase, password, ) => { + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-import-wallet"]'); @@ -293,6 +299,9 @@ const completeImportSRPOnboardingFlowWordByWord = async ( }; const completeCreateNewWalletOnboardingFlow = async (driver, password) => { + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-create-wallet"]'); @@ -342,6 +351,9 @@ const completeCreateNewWalletOnboardingFlow = async (driver, password) => { }; const importWrongSRPOnboardingFlow = async (driver, seedPhrase) => { + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-import-wallet"]'); diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 6cc81b4008d7..e3a53ffdd0e5 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -84,6 +84,7 @@ describe('MetaMask', function () { describe('Going through the first time flow', function () { it('clicks the "Create New Wallet" button on the welcome screen', async function () { + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]'); }); diff --git a/test/e2e/tests/add-custom-network.spec.js b/test/e2e/tests/add-custom-network.spec.js index 3564f9f2e5e1..45bdc8e36449 100644 --- a/test/e2e/tests/add-custom-network.spec.js +++ b/test/e2e/tests/add-custom-network.spec.js @@ -392,7 +392,9 @@ describe('Custom network', function () { text: 'Delete', }); - await driver.findElement('.modal-container__footer'); + await driver.waitForSelector('.modal-container__footer', { + timeout: 15000, + }); // should be deleted from the modal shown again to complete deletion custom network await driver.clickElement({ tag: 'button', diff --git a/test/e2e/tests/custom-token-add-approve.spec.js b/test/e2e/tests/custom-token-add-approve.spec.js index 5e1addcc5ca1..c3e73d71e451 100644 --- a/test/e2e/tests/custom-token-add-approve.spec.js +++ b/test/e2e/tests/custom-token-add-approve.spec.js @@ -292,10 +292,13 @@ describe('Create token, approve token and approve token without gas', function ( await gasLimitInput.fill('60001'); await driver.clickElement({ text: 'Save', tag: 'button' }); - await driver.waitForSelector({ - css: '.box--flex-direction-row > h6', - text: '0.0006 ETH', - }); + await driver.waitForSelector( + { + css: '.box--flex-direction-row > h6', + text: '0.0006 ETH', + }, + { timeout: 15000 }, + ); // editing spending cap await driver.clickElement({ diff --git a/test/e2e/tests/incremental-security.spec.js b/test/e2e/tests/incremental-security.spec.js index 2daf70a91858..2213046e3b9f 100644 --- a/test/e2e/tests/incremental-security.spec.js +++ b/test/e2e/tests/incremental-security.spec.js @@ -29,6 +29,8 @@ describe('Incremental Security', function () { }, async ({ driver }) => { await driver.navigate(); + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); // welcome await driver.clickElement('[data-testid="onboarding-create-wallet"]'); diff --git a/test/e2e/tests/metamask-responsive-ui.spec.js b/test/e2e/tests/metamask-responsive-ui.spec.js index 4c83081c2ba8..e21668a103e2 100644 --- a/test/e2e/tests/metamask-responsive-ui.spec.js +++ b/test/e2e/tests/metamask-responsive-ui.spec.js @@ -15,6 +15,8 @@ describe('MetaMask Responsive UI', function () { }, async ({ driver }) => { await driver.navigate(); + // agree to terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); // welcome await driver.clickElement('[data-testid="onboarding-create-wallet"]'); diff --git a/test/e2e/tests/onboarding.spec.js b/test/e2e/tests/onboarding.spec.js index 7778989d3c56..260857ce9c43 100644 --- a/test/e2e/tests/onboarding.spec.js +++ b/test/e2e/tests/onboarding.spec.js @@ -108,6 +108,9 @@ describe('MetaMask onboarding', function () { async ({ driver }) => { await driver.navigate(); + // accept terms of use + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); + // welcome await driver.clickElement('[data-testid="onboarding-import-wallet"]'); @@ -145,6 +148,7 @@ describe('MetaMask onboarding', function () { async ({ driver }) => { await driver.navigate(); + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]'); // metrics @@ -208,6 +212,7 @@ describe('MetaMask onboarding', function () { async ({ driver }) => { await driver.navigate(); + await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]'); // metrics diff --git a/test/e2e/tests/send-eth.spec.js b/test/e2e/tests/send-eth.spec.js index b5542c1cba70..d287184c3e8f 100644 --- a/test/e2e/tests/send-eth.spec.js +++ b/test/e2e/tests/send-eth.spec.js @@ -354,9 +354,12 @@ describe('Send ETH from dapp using advanced gas controls', function () { '.transaction-list-item__primary-currency', ); await txValue.click(); - const baseFeeValue = await driver.waitForSelector({ - text: '0.000000025', - }); + const baseFeeValue = await driver.waitForSelector( + { + text: '0.000000025', + }, + { timeout: 15000 }, + ); assert.equal(await baseFeeValue.getText(), '0.000000025'); }, ); diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index adb1ff1f6066..0fb69b866fc5 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -83,6 +83,7 @@ @import 'transaction-status-label/index'; @import 'wallet-overview/index'; @import 'whats-new-popup/index'; +@import 'terms-of-use-popup/index'; @import 'loading-network-screen/index'; @import 'transaction-decoding/index'; @import 'advanced-gas-fee-popover/index'; diff --git a/ui/components/app/app-header/app-header.component.js b/ui/components/app/app-header/app-header.component.js index 248dda89f470..6ef03c773fb6 100644 --- a/ui/components/app/app-header/app-header.component.js +++ b/ui/components/app/app-header/app-header.component.js @@ -89,6 +89,7 @@ export default class AppHeader extends PureComponent { className={classnames('account-menu__icon', { 'account-menu__icon--disabled': disabled, })} + disabled={Boolean(disabled)} onClick={() => { if (!disabled) { !isAccountMenuOpen && diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.js b/ui/components/app/custom-spending-cap/custom-spending-cap.js index 12f1ddce431b..7f4a46c26225 100644 --- a/ui/components/app/custom-spending-cap/custom-spending-cap.js +++ b/ui/components/app/custom-spending-cap/custom-spending-cap.js @@ -1,4 +1,4 @@ -import React, { useState, useContext, useEffect } from 'react'; +import React, { useState, useContext, useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -41,6 +41,7 @@ export default function CustomSpendingCap({ }) { const t = useContext(I18nContext); const dispatch = useDispatch(); + const inputRef = useRef(null); const value = useSelector(getCustomTokenAmount); @@ -139,6 +140,15 @@ export default function CustomSpendingCap({ passTheErrorText(error); }, [error, passTheErrorText]); + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus({ + preventScroll: true, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inputRef.current]); + const chooseTooltipContentText = decConversionGreaterThan( value, currentTokenBalance, @@ -182,8 +192,8 @@ export default function CustomSpendingCap({ } > + {getWeightedPermissions(t, permissions, targetSubjectMetadata).map( + (permission, index) => { + return ( + + ); + }, + )} + + ); +} + +SnapPermissionsList.propTypes = { + permissions: PropTypes.object.isRequired, + targetSubjectMetadata: PropTypes.object.isRequired, +}; diff --git a/ui/components/app/flask/snap-permissions-list/snap-permissions-list.stories.js b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.stories.js new file mode 100644 index 000000000000..e3f24287e874 --- /dev/null +++ b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.stories.js @@ -0,0 +1,37 @@ +import React from 'react'; + +import SnapPermissionsList from '.'; + +export default { + title: 'Components/App/flask/SnapPermissionsList', + + component: SnapPermissionsList, + argTypes: { + permissions: { + control: 'object', + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; + +DefaultStory.args = { + permissions: { + eth_accounts: {}, + snap_dialog: {}, + snap_getBip32PublicKey: { + caveats: [ + { + value: [ + { + path: ['m', `44'`, `0'`], + curve: 'secp256k1', + }, + ], + }, + ], + }, + }, +}; diff --git a/ui/components/app/flask/snap-permissions-list/snap-permissions-list.test.js b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.test.js new file mode 100644 index 000000000000..4ba174224d41 --- /dev/null +++ b/ui/components/app/flask/snap-permissions-list/snap-permissions-list.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { renderWithProvider } from '../../../../../test/jest'; +import SnapPermissionsList from './snap-permissions-list'; + +describe('Snap Permission List', () => { + const mockPermissionData = { + snap_dialog: { + caveats: null, + date: 1680709920602, + id: '4dduR1BpsmS0ZJfeVtiAh', + invoker: 'local:http://localhost:8080', + parentCapability: 'snap_dialog', + }, + }; + const mockTargetSubjectMetadata = { + extensionId: null, + iconUrl: null, + name: 'TypeScript Example Snap', + origin: 'local:http://localhost:8080', + subjectType: 'snap', + version: '0.2.2', + }; + + it('renders permissions list for snaps', () => { + renderWithProvider( + , + ); + expect( + screen.getByText('Display dialog windows in MetaMask.'), + ).toBeInTheDocument(); + expect(screen.getByText('Approved on 2023-04-05')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/menu-bar/menu-bar.js b/ui/components/app/menu-bar/menu-bar.js index fac87c26fa25..08da8208f5ac 100644 --- a/ui/components/app/menu-bar/menu-bar.js +++ b/ui/components/app/menu-bar/menu-bar.js @@ -16,7 +16,6 @@ import { getOriginOfCurrentTab } from '../../../selectors'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { ButtonIcon } from '../../component-library/button-icon/deprecated'; import { ICON_NAMES } from '../../component-library/icon/deprecated'; -import { GlobalMenu } from '../../multichain/global-menu'; import AccountOptionsMenu from './account-options-menu'; export default function MenuBar() { @@ -34,7 +33,7 @@ export default function MenuBar() { return (
- {showStatus ? ( // TODO: Move the connection status menu icon to the correct position in header once we implement the new header + {showStatus ? ( history.push(CONNECTED_ACCOUNTS_ROUTE)} /> @@ -58,18 +57,12 @@ export default function MenuBar() { }} /> - {accountOptionsMenuOpen && - (process.env.MULTICHAIN ? ( - setAccountOptionsMenuOpen(false)} - /> - ) : ( - setAccountOptionsMenuOpen(false)} - /> - ))} + {accountOptionsMenuOpen && ( + setAccountOptionsMenuOpen(false)} + /> + )}
); } diff --git a/ui/components/app/permission-cell/permission-cell.js b/ui/components/app/permission-cell/permission-cell.js index ea87a381e16a..fbbafea434d8 100644 --- a/ui/components/app/permission-cell/permission-cell.js +++ b/ui/components/app/permission-cell/permission-cell.js @@ -53,6 +53,11 @@ const PermissionCell = ({ iconBackgroundColor = Color.backgroundAlternative; } + let permissionIcon = avatarIcon; + if (typeof avatarIcon !== 'string' && avatarIcon?.props?.iconName) { + permissionIcon = avatarIcon.props.iconName; + } + return ( - {typeof avatarIcon === 'string' ? ( + {typeof permissionIcon === 'string' ? ( ) : ( - avatarIcon + permissionIcon )} diff --git a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js index ffbbca22dec0..accab2e15229 100644 --- a/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js +++ b/ui/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -28,11 +28,14 @@ export default class PermissionPageContainerContent extends PureComponent { }; renderRequestedPermissions() { - const { selectedPermissions } = this.props; + const { selectedPermissions, subjectMetadata } = this.props; return (
- +
); } diff --git a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js index 5c5201f60af6..6d0ccfcb77d9 100644 --- a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js +++ b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js @@ -1,9 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { getWeightedPermissions } from '../../../helpers/utils/permission'; +import { + getRightIcon, + getWeightedPermissions, +} from '../../../helpers/utils/permission'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import PermissionCell from '../permission-cell'; -import Box from '../../ui/box'; + +/** + * Get one or more permission descriptions for a permission name. + * + * @param permission - The permission to render. + * @param index - The index of the permission. + * @returns {JSX.Element} A permission description node. + */ +function getDescriptionNode(permission, index) { + const { label, leftIcon, permissionName } = permission; + + return ( +
+ {typeof leftIcon === 'string' ? : leftIcon} + {label} + {getRightIcon(permission)} +
+ ); +} export default function PermissionsConnectPermissionList({ permissions, @@ -12,22 +32,11 @@ export default function PermissionsConnectPermissionList({ const t = useI18nContext(); return ( - +
{getWeightedPermissions(t, permissions, targetSubjectMetadata).map( - (permission, index) => { - return ( - - ); - }, + getDescriptionNode, )} - +
); } diff --git a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.stories.js b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.stories.js index dd5cfb9ba9de..9ee800b0328f 100644 --- a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.stories.js +++ b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.stories.js @@ -20,18 +20,5 @@ DefaultStory.storyName = 'Default'; DefaultStory.args = { permissions: { eth_accounts: {}, - snap_dialog: {}, - snap_getBip32PublicKey: { - caveats: [ - { - value: [ - { - path: ['m', `44'`, `0'`], - curve: 'secp256k1', - }, - ], - }, - ], - }, }, }; diff --git a/ui/components/app/signature-request-siwe/signature-request-siwe.js b/ui/components/app/signature-request-siwe/signature-request-siwe.js index 11e515eeecd8..de7441cb7b95 100644 --- a/ui/components/app/signature-request-siwe/signature-request-siwe.js +++ b/ui/components/app/signature-request-siwe/signature-request-siwe.js @@ -7,6 +7,7 @@ import Popover from '../../ui/popover'; import Checkbox from '../../ui/check-box'; import { I18nContext } from '../../../contexts/i18n'; import { PageContainerFooter } from '../../ui/page-container'; +import { isAddressLedger } from '../../../ducks/metamask/metamask'; import { accountsWithSendEtherInfoSelector, getSubjectMetadata, @@ -20,6 +21,7 @@ import { import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message'; import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../security-provider-banner-message/security-provider-banner-message.constants'; +import LedgerInstructionField from '../ledger-instruction-field'; import Header from './signature-request-siwe-header'; import Message from './signature-request-siwe-message'; @@ -39,6 +41,8 @@ export default function SignatureRequestSIWE({ }, } = txData; + const isLedgerWallet = useSelector((state) => isAddressLedger(state, from)); + const fromAccount = getAccountByAddress(allAccounts, from); const targetSubjectMetadata = subjectMetadata[origin]; @@ -115,6 +119,13 @@ export default function SignatureRequestSIWE({ ])} )} + + {isLedgerWallet && ( +
+ +
+ )} + {!isSIWEDomainValid && ( { + return { + __esModule: true, + default: () => { + return
; + }, + }; +}); + const render = (txData = mockProps.txData) => { const store = configureStore(mockStoreInitialState); @@ -110,4 +119,21 @@ describe('SignatureRequestSIWE (Sign in with Ethereum)', () => { expect(bannerAlert).toBeTruthy(); expect(bannerAlert).toHaveTextContent('Deceptive site request.'); }); + + it('should not show Ledger instructions if the address is not a Ledger address', () => { + const { container } = render(); + expect( + container.querySelector('.mock-ledger-instruction-field'), + ).not.toBeTruthy(); + }); + + it('should show Ledger instructions if the address is a Ledger address', () => { + const mockTxData = cloneDeep(mockProps.txData); + mockTxData.msgParams.from = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'; + const { container } = render(mockTxData); + + expect( + container.querySelector('.mock-ledger-instruction-field'), + ).toBeTruthy(); + }); }); diff --git a/ui/components/app/terms-of-use-popup/index.js b/ui/components/app/terms-of-use-popup/index.js new file mode 100644 index 000000000000..e9333ccd17aa --- /dev/null +++ b/ui/components/app/terms-of-use-popup/index.js @@ -0,0 +1 @@ +export { default } from './terms-of-use-popup'; diff --git a/ui/components/app/terms-of-use-popup/index.scss b/ui/components/app/terms-of-use-popup/index.scss new file mode 100644 index 000000000000..7ba93df4ab3c --- /dev/null +++ b/ui/components/app/terms-of-use-popup/index.scss @@ -0,0 +1,30 @@ +.popover-wrap.terms-of-use__popover { + .terms-of-use { + &__terms-list { + list-style: decimal none outside; + } + + &__footer-text { + align-self: center; + } + } + + .popover-header { + &__title { + margin-bottom: 0; + } + } + + .popover-footer { + border-top: none; + } + + @include screen-sm-min { + max-height: 750px; + width: 500px; + } + + @include screen-sm-max { + max-height: 568px; + } +} diff --git a/ui/components/app/terms-of-use-popup/terms-of-use-popup.js b/ui/components/app/terms-of-use-popup/terms-of-use-popup.js new file mode 100644 index 000000000000..9fe664303895 --- /dev/null +++ b/ui/components/app/terms-of-use-popup/terms-of-use-popup.js @@ -0,0 +1,1181 @@ +import React, { useContext, useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { I18nContext } from '../../../contexts/i18n'; +import Popover from '../../ui/popover'; +import { + AlignItems, + FLEX_DIRECTION, + TextVariant, + Color, + TextColor, +} from '../../../helpers/constants/design-system'; +import { + Text, + Button, + BUTTON_TYPES, + ButtonLink, + Label, +} from '../../component-library'; +import Box from '../../ui/box'; +import CheckBox from '../../ui/check-box/check-box.component'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; + +export default function TermsOfUsePopup({ onAccept }) { + const t = useContext(I18nContext); + const trackEvent = useContext(MetaMetricsContext); + const [isTermsOfUseChecked, setIsTermsOfUseChecked] = useState(false); + + const popoverRef = useRef(); + const bottomRef = React.createRef(); + + const handleScrollDownClick = (e) => { + e.stopPropagation(); + bottomRef.current.scrollIntoView({ + behavior: 'smooth', + }); + }; + + useEffect(() => { + trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.TermsOfUseShown, + properties: { + location: 'Terms Of Use Popover', + }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + + {t('termsOfUseFooterText')} + + + } + > + + + + IMPORTANT NOTICE: THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION + AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 11. + PLEASE READ THE AGREEMENT CAREFULLY. + + + ConsenSys Software Inc. (“ConsenSys,” “we,” “us,” or “our”) is the + leading blockchain software development company. With a focus on + utilizing decentralized technologies, such as Ethereum, our software + is powering a revolution in commerce and finance and helping to + optimize business processes. ConsenSys hosts a top level domain + website, www.consensys.net, that serves information regarding + ConsenSys and our Offerings, as defined below, as well as + sub-domains for our products or services (the top level domain with + the sub-domains collectively referred to as the “Site”), which + include text, images, audio, code and other materials or third party + information.  + + + These Terms of Use (the “Terms,” “Terms of Use” or “Agreement”) + contain the terms and conditions that govern your access to and use + of the Site and Offerings provided by us and is an agreement between + us and you or the entity you represent (“you” or “your”). Please + read these Terms of Use carefully before using the Site or + Offerings. By using the Site, clicking a button or checkbox to + accept or agree to these Terms where that option is made available, + clicking a button to use or access any of the Offerings, completing + an Order, or,  if earlier, using or otherwise accessing the + Offerings (the date on which any of the events listed above occur + being the “Effective Date”), you (1) accept and agree to these Terms + and any additional terms, rules and conditions of participation + issued by ConsenSys from time to time and (2) consent to the + collection, use, disclosure and other handling of information as + described in our{' '} + + Privacy Policy. + {' '} + If you do not agree to the Terms or perform any and all obligations + you accept under the Terms, then you may not access or use the + Offerings.  + + + You represent to us that you are lawfully able to enter into + contracts. If you are entering into this Agreement for an entity, + such as the company you work for, you represent to us that you have + legal authority to bind that entity. Please see Section 13 for + definitions of certain capitalized terms used in this Agreement. + + + In addition, you represent to us that you and your financial + institutions, or any party that owns or controls you or your + financial institutions, are (1) not subject to sanctions or + otherwise designated on any list of prohibited or restricted + parties, including but not limited to the lists maintained by the + United Nations Security Council, the U.S. Government (i.e., the + Specially Designated Nationals List and Foreign Sanctions Evaders + List of the U.S. Department of Treasury and the Entity List of the + U.S. Department of Commerce), the European Union or its Member + States, or other applicable government authority and (2) not located + in any country subject to a comprehensive sanctions program + implemented by the United States. + + + 1. The Offerings. + + + 1.1 Generally. You may access and use the Offerings in accordance + with this Agreement. You agree to comply with the terms of this + Agreement and all laws, rules and regulations applicable to your use + of the Offerings. + + + 1.2 Offerings and Access. ConsenSys offers a number of products and + services, each an “Offering”, under the ConsenSys brand or brands + owned by us. These include Codefi, Quorum, Infura, MetaMask and + others. Offerings are generally accessed through the Site or through + a third party provider of which we approved, such as the Google Play + or Apple App Store, unless otherwise agreed in writing. Some + Offerings may require you to create an account, enter a valid form + of payment, and select a paid plan (a “Paid Plan”), or initiate an + Order.  + + + 1.3 Third-Party Content. In certain Offerings, Third-Party Content + may be used by you at your election. Third-Party Content is governed + by this Agreement and, if applicable, separate terms and conditions + accompanying such Third-Party Content, which terms and conditions + may include separate fees and charges. + + + 1.4 Third-Party Offerings. When you use our Offerings, you may also + be using the products or services of one or more third parties. Your + use of these third party offerings may be subject to the separate + policies, terms of use, and fees of these third parties. + + + 2. Changes. + + + 2.1 To the Offerings. We may change or discontinue any or all of the + Offerings or change or remove functionality of any or all of the + Offerings from time to time. We will use commercially reasonable + efforts to communicate to you any material change or discontinuation + of an Offering through the Site or public communication + channels.  If you are on a Paid Plan, we will use commercially + reasonable efforts to communicate to you  any material changes + to or discontinuation of the Offering at least 30 days in advance of + such change, and we will use commercially reasonable efforts to + continue supporting the previous version of the Offering for up to + three months after the change or discontinuation, except if doing so + (a) would pose an information security or intellectual property + issue, (b) is economically or technically burdensome, or (c) would + create undue risk of us violating the law. + + + 2.2 To this Agreement. We reserve the right, at our sole discretion, + to modify or replace any part of this Agreement or any Policies at + any time. It is your responsibility to check this Agreement + periodically for changes, but we will also use commercially + reasonable efforts to communicate any material changes to this + Agreement through the Site or other public channels. Your continued + use of or access to the Offerings following the posting of any + changes to this Agreement constitutes acceptance of those changes. + + + 3. Your Responsibilities. + + + 3.1 Your Accounts.  For those Offerings that require an + account, and except to the extent caused by our breach of this + Agreement, (a) you are responsible for all activities that occur + under your account, regardless of whether the activities are + authorized by you or undertaken by you, your employees or a third + party (including your contractors, agents or other End Users), and + (b) we and our affiliates are not responsible for unauthorized + access to your account, including any access that occurred as a + result of fraud, phishing, or other criminal activity perpetrated by + third parties.   + + + 3.2 Your Use. You are responsible for all activities that occur + through your use of those Offerings that do not require an account, + except to the extent caused by our breach of this Agreement, + regardless of whether the activities are authorized by you or + undertaken by you, your employees or a third party (including your + contractors, agents or other End Users).  We and our affiliates + are not responsible for unauthorized access that may occur during + your use of the Offerings, including any access that occurred as a + result of fraud, phishing, or other criminal activity perpetrated by + third parties.  You will ensure that your use of the Offerings + does not violate any applicable law.   + + + 3.3 Your Security and Backup. You are solely responsible for + properly configuring and using the Offerings and otherwise taking + appropriate action to secure, protect and backup your accounts + and/or Your Content in a manner that will provide appropriate + security and protection, which might include use of + encryption.  This includes your obligation under this Agreement + to record and securely maintain any passwords or backup security + phrases (i.e. “seed” phrases) that relate to your use of the + Offerings. You acknowledge that you will not share with us nor any + other third party any password or backup/seed phrase that relates to + your use of the Offerings, and that we will not be held responsible + if you do share any such phrase or password. + + + 3.4 Log-In Credentials and API Authentication. To the extent we + provide you with log-in credentials and API authentication generated + by the Offerings, such log-in credentials and API authentication are + for your use only and you will not sell, transfer or sublicense them + to any other entity or person, except that you may disclose your + password or private key to your agents and subcontractors performing + work on your behalf. + + + 3.5 Applicability to MetaMask Offerings. For the avoidance of doubt, + the terms of this Section 3 are applicable to all Offerings, + including MetaMask and any accounts you create through MetaMask with + Third Party Offerings, such as decentralized applications, or + blockchain-based accounts themselves. + + + 4. Fees and Payment. + + + 4.1 Publicly Available Offerings. Some Offerings may be offered to + the public and licensed on a royalty free basis, including Offerings + that require a Paid Plan for software licensing fees above a certain + threshold of use.  + + + 4.2 Offering Fees.  If your use of an Offering does not require + an Order or Paid Plan but software licensing fees are charged + contemporaneously with your use of the Offering, those fees will be + charged as described on the Site or in the user interface of the + Offering.  Such fees may be calculated by combining a fee + charged by us and a fee charged by a Third Party Offering that + provides certain functionality related to the Offering.  For + those Offerings which entail an Order or Paid Plan, we calculate and + bill fees and charges according to your Order or Paid Plan. For such + Offerings, on the first day of each billing period, you will pay us + the applicable fees (the “Base Fees”) and any applicable taxes based + on the Offerings in the Paid Plan. In addition, we may, for + particular Orders, issue an invoice to you for all charges above the + applicable threshold for your Paid Plan which constitute overage + fees for the previous billing period. If you make any other changes + to the Offerings during a billing period (e.g. upgrading or + downgrading your Paid Plan), we will apply any additional charges or + credits to the next billing period. We may bill you more frequently + for fees accrued at our discretion upon notice to you.  You + will pay all fees in U.S. dollars unless the particular Offering + specifies a different form of payment or otherwise agreed to in + writing. All amounts payable by you under this Agreement will be + paid to us without setoff or counterclaim, and without any deduction + or withholding. Fees and charges for any new Offering or new feature + of an Offering will be effective when we use commercially reasonable + efforts to communicate updated fees and charges through our Site or + other public channels or, if you are on a Paid Plan, upon + commercially reasonable efforts to notify you, unless we expressly + state otherwise in a notice. We may increase or add new fees and + charges for any existing Offerings you are using by using + commercially reasonable efforts to notify users of the Offerings + through our Site or other public channels or, if you are on a Paid + Plan, by giving you at least 30 days’ prior notice.  Unless + otherwise specified in an Order, if you are on a Paid Plan, all + amounts due under this Agreement are payable within thirty (30) days + following receipt of your invoice.  We may elect to charge you + interest at the rate of 1.5% per month (or the highest rate + permitted by law, if less) on all late payments. + + + 4.3 Taxes. Each party will be responsible, as required under + applicable law, for identifying and paying all taxes and other + governmental fees and charges (and any penalties, interest, and + other additions thereto) that are imposed on that party upon or with + respect to the transactions and payments under this Agreement. All + fees payable by you are exclusive taxes unless otherwise noted. We + reserve the right to withhold taxes where required. + + + 5. Temporary Suspension; Limiting API Requests. + + + 5.1 Generally. We may suspend your right to access or use any + portion or all of the Offerings immediately if we determine: + + + (a) your use of the Offerings (i) poses a security risk to the + Offerings or any third party, (ii) could adversely impact our + systems, the Offerings or the systems of any other user, (iii) could + subject us, our affiliates, or any third party to liability, or (iv) + could be unlawful; + + + (b) you are, or any End User is, in breach of this Agreement; + + + (c) you are in breach of your payment obligations under Section 4 + and such breach continues for 30 days or longer; or + + + (d) for entities, you have ceased to operate in the ordinary course, + made an assignment for the benefit of creditors or similar + disposition of your assets, or become the subject of any bankruptcy, + reorganization, liquidation, dissolution or similar proceeding. + + + 5.2 Effect of Suspension. If we suspend your right to access or use + any portion or all of the Offerings: + + + (a) you remain responsible for all fees and charges you incur during + the period of suspension; and + + + (b) you will not be entitled to any fee credits for any period of + suspension. + + + 5.3 Limiting API Requests. If applicable to a particular Offering, + we retain sole discretion to limit your usage of the Offerings + (including without limitation by limiting the number of API requests + you may submit (“API Requests”)) at any time if your usage of the + Offerings exceeds the usage threshold specified in your Paid + Plan.    + + + 6. Term; Termination. + + + 6.1 Term. For Offerings subject to a Paid Plan, the term of this + Agreement will commence on the Effective Date and will remain in + effect until terminated under this Section 6. Any notice of + termination of this Agreement by either party to the other must + include a Termination Date that complies with the notice periods in + Section 6.2.  For Offerings that are not subject to a Paid + Plan, the term of this Agreement will commence on the Effective Date + and will remain in effect until you stop accessing or using the + Offerings.  + + + 6.2 Termination. + + + (a) Termination for Convenience. If you are not on a Paid Plan, you + may terminate this Agreement for any reason by ceasing use of the + Offering. If you are on a Paid Plan, each party may terminate this + Agreement for any reason by giving the other party at least 30 days’ + written notice, subject to the provisions in Section 6.2(b). + + + (b) Termination for Cause. + + + (i) By Either Party. Either party may terminate this Agreement for + cause if the other party is in material breach of this Agreement and + the material breach remains uncured for a period of 30 days from + receipt of notice by the other party.  + + + (ii) By Us. We may also terminate this Agreement immediately (A) for + cause if we have the right to suspend under Section 5, (B) if our + relationship with a third-party partner who provides software or + other technology we use to provide the Offerings expires, terminates + or requires us to change the way we provide the software or other + technology as part of the Offerings, or (C) in order to avoid undue + risk of violating the law. + + + 6.3 Effect of Termination. Upon the Termination Date: + + + (i) all your rights under this Agreement immediately terminate; and + + + (ii) each party remains responsible for all fees and charges it has + incurred through the Termination Date and are responsible for any + fees and charges it incurs during the post-termination period; + + + (iii) the terms and conditions of this Agreement shall survive the + expiration or termination of this Agreement to the full extent + necessary for their enforcement and for the protection of the party + in whose favor they operate.  For instance, despite this + Agreement between you and us terminating, any dispute raised after + you stop accessing or using the Offerings will be subject to the + applicable provisions of this Agreement if that dispute relates to + your prior access or use. + + + For any use of the Offerings after the Termination Date, the terms + of this Agreement will again apply and, if your use is under a Paid + Plan, you will pay the applicable fees at the rates under Section 4. + + + 7. Proprietary Rights. + + + 7.1 Your Content. Depending on the Offering, you may share Content + with us. Except as provided in this Section 7, we obtain no rights + under this Agreement from you (or your licensors) to Your Content. + You consent to our use of Your Content to provide the Offerings to + you. + + + 7.2 Offerings License. We or our licensors own all right, title, and + interest in and to the Offerings, and all related technology and + intellectual property rights. Subject to the terms of this + Agreement, we grant you a limited, revocable, non-exclusive, + non-sublicensable, non-transferable license to do the following: (a) + access and use the Offerings solely in accordance with this + Agreement; and (b) copy and use Our Content solely in connection + with your permitted use of the Offerings. Except as provided in this + Section 7.2, you obtain no rights under this Agreement from us, our + affiliates or our licensors to the Offerings, including any related + intellectual property rights. Some of Our Content and Third-Party + Content may be provided to you under a separate license, such as the + Apache License, Version 2.0, or other open source license. In the + event of a conflict between this Agreement and any separate license, + the separate license will prevail with respect to Our Content or + Third-Party Content that is the subject of such separate license. + + + 7.3 License Restrictions. Neither you nor any End User will use the + Offerings in any manner or for any purpose other than as expressly + permitted by this Agreement. Except for as authorized, neither you + nor any End User will, or will attempt to (a) modify, distribute, + alter, tamper with, repair, or otherwise create derivative works of + any Content included in the Offerings (except to the extent Content + included in the Offerings is provided to you under a separate + license that expressly permits the creation of derivative works), + (b) reverse engineer, disassemble, or decompile the Offerings or + apply any other process or procedure to derive the source code of + any software included in the Offerings (except to the extent + applicable law doesn’t allow this restriction), (c) access or use + the Offerings in a way intended to avoid incurring fees or exceeding + usage limits or quotas, (d) use scraping techniques to mine or + otherwise scrape data except as permitted by a Plan, or (e) resell + or sublicense the Offerings unless otherwise agreed in writing. You + will not use Our Marks unless you obtain our prior written consent. + You will not misrepresent or embellish the relationship between us + and you (including by expressing or implying that we support, + sponsor, endorse, or contribute to you or your business endeavors). + You will not imply any relationship or affiliation between us and + you except as expressly permitted by this Agreement. + + + 7.4 Suggestions. If you provide any Suggestions to us or our + affiliates, we and our affiliates will be entitled to use the + Suggestions without restriction. You hereby irrevocably assign to us + all right, title, and interest in and to the Suggestions and agree + to provide us any assistance we require to document, perfect, and + maintain our rights in the Suggestions. + + + 7.5 U.S. Government Users. If you are a U.S. Government End User, we + are licensing the Offerings to you as a “Commercial Item” as that + term is defined in the U.S. Code of Federal Regulations (see 48 + C.F.R. § 2.101), and the rights we grant you to the Offerings are + the same as the rights we grant to all others under these Terms of + Use. + + + 8. Indemnification. + + + 8.1 General.  + + + (a) You will defend, indemnify, and hold harmless us, our affiliates + and licensors, and each of their respective employees, officers, + directors, and representatives from and against any Losses arising + out of or relating to any claim concerning: (a) breach of this + Agreement or violation of applicable law by you; and (b) a dispute + between you and any of your customers or users. You will reimburse + us for reasonable attorneys’ fees and expenses, associated with + claims described in (a) and (b) above. + + + (b) We will defend, indemnify, and hold harmless you and your + employees, officers, directors, and representatives from and against + any Losses arising out of or relating to any claim concerning our + material and intentional breach of this Agreement.  We will + reimburse you for reasonable attorneys’ fees and expenses associated + with the claims described in this paragraph. + + + 8.2 Intellectual Property. + + + (a) Subject to the limitations in this Section 8, you will defend + ConsenSys, its affiliates, and their respective employees, officers, + and directors against any third-party claim alleging that any of + Your Content infringes or misappropriates that third party’s + intellectual property rights, and will pay the amount of any adverse + final judgment or settlement. + + + (b) Subject to the limitations in this Section 8 and the limitations + in Section 10, we will defend you and your employees, officers, and + directors against any third-party claim alleging that the Offerings + infringe or misappropriate that third party’s intellectual property + rights, and will pay the amount of any adverse final judgment or + settlement.  However, we will not be required to spend more + than $200,000 pursuant to this Section 8, including without + limitation attorneys’ fees, court costs, settlements, judgments, and + reimbursement costs. + + + (c) Neither party will have obligations or liability under this + Section 8.2 arising from infringement by you combining the Offerings + with any other product, service, software, data, content or method. + In addition, we will have no obligations or liability arising from + your use of the Offerings after we have notified you to discontinue + such use. The remedies provided in this Section 8.2 are the sole and + exclusive remedies for any third-party claims of infringement or + misappropriation of intellectual property rights by the Offerings or + by Your Content. + + + 8.3 Process. In no event will a party agree to any settlement of any + claim that involves any commitment, other than the payment of money, + without the written consent of the other party. + + + 9. Disclaimers; Risk. + + + 9.1 DISCLAIMER. THE OFFERINGS ARE PROVIDED “AS IS.” EXCEPT TO THE + EXTENT PROHIBITED BY LAW, OR TO THE EXTENT ANY STATUTORY RIGHTS + APPLY THAT CANNOT BE EXCLUDED, LIMITED OR WAIVED, WE AND OUR + AFFILIATES AND LICENSORS (A) MAKE NO REPRESENTATIONS OR WARRANTIES + OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE + REGARDING THE OFFERINGS OR THE THIRD-PARTY CONTENT, AND (B) DISCLAIM + ALL WARRANTIES, INCLUDING ANY IMPLIED OR EXPRESS WARRANTIES (I) OF + MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, OR QUIET ENJOYMENT, (II) ARISING OUT OF + ANY COURSE OF DEALING OR USAGE OF TRADE, (III) THAT THE OFFERINGS OR + THIRD-PARTY CONTENT WILL BE UNINTERRUPTED, ERROR FREE OR FREE OF + HARMFUL COMPONENTS, AND (IV) THAT ANY CONTENT WILL BE SECURE OR NOT + OTHERWISE LOST OR ALTERED. + + + 9.2 RISKS. OUR OFFERINGS RELY ON EMERGING TECHNOLOGIES, SUCH AS + ETHEREUM. SOME OFFERINGS ARE SUBJECT TO INCREASED RISK THROUGH YOUR + POTENTIAL MISUSE OF THINGS SUCH AS PUBLIC/PRIVATE KEY CRYPTOGRAPHY, + OR FAILING TO PROPERLY UPDATE OR RUN SOFTWARE TO ACCOMMODATE + PROTOCOL UPGRADES, LIKE THE TRANSITION TO PROOF OF STAKE CONSENSUS. + BY USING THE OFFERINGS YOU EXPLICITLY ACKNOWLEDGE AND ACCEPT THESE + HEIGHTENED RISKS.  YOU REPRESENT THAT YOU ARE FINANCIALLY AND + TECHNICALLY SOPHISTICATED ENOUGH TO UNDERSTAND THE INHERENT RISKS + ASSOCIATED WITH USING CRYPTOGRAPHIC AND BLOCKCHAIN-BASED SYSTEMS AND + UPGRADING YOUR SOFTWARE AND PROCESSES TO ACCOMMODATE PROTOCOL + UPGRADES, AND THAT YOU HAVE A WORKING KNOWLEDGE OF THE USAGE AND + INTRICACIES OF DIGITAL ASSETS SUCH AS ETHER (ETH) AND OTHER DIGITAL + TOKENS, SUCH AS THOSE FOLLOWING THE ERC-20 TOKEN STANDARD.  IN + PARTICULAR, YOU UNDERSTAND THAT WE DO NOT OPERATE THE ETHEREUM + PROTOCOL OR ANY OTHER BLOCKCHAIN PROTOCOL, COMMUNICATE OR EXECUTE + PROTOCOL UPGRADES, OR APPROVE OR PROCESS BLOCKCHAIN TRANSACTIONS ON + BEHALF OF YOU.  YOU FURTHER UNDERSTAND THAT BLOCKCHAIN + PROTOCOLS PRESENT THEIR OWN RISKS OF USE, THAT SUPPORTING OR + PARTICIPATING IN THE PROTOCOL MAY RESULT IN LOSSES IF YOUR + PARTICIPATION VIOLATES CERTAIN PROTOCOL RULES, THAT  + BLOCKCHAIN-BASED TRANSACTIONS ARE IRREVERSIBLE, THAT YOUR PRIVATE + KEY AND BACKUP SEED PHRASE MUST BE KEPT SECRET AT ALL TIMES, THAT + CONSENSYS WILL NOT STORE A BACKUP OF, NOR WILL BE ABLE TO DISCOVER + OR RECOVER, YOUR PRIVATE KEY OR BACKUP SEED PHRASE, AND THAT YOU ARE + SOLELY RESPONSIBLE FOR ANY APPROVALS OR PERMISSIONS YOU PROVIDE BY + CRYPTOGRAPHICALLY SIGNING BLOCKCHAIN MESSAGES OR TRANSACTIONS. + + + YOU FURTHER UNDERSTAND AND ACCEPT THAT DIGITAL TOKENS PRESENT MARKET + VOLATILITY RISK, TECHNICAL SOFTWARE RISKS, REGULATORY RISKS, AND + CYBERSECURITY RISKS.  YOU UNDERSTAND THAT THE COST AND SPEED OF + A BLOCKCHAIN-BASED SYSTEM IS VARIABLE, THAT COST MAY INCREASE + DRAMATICALLY AT ANY TIME, AND THAT COST AND SPEED IS NOT WITHIN THE + CAPABILITY OF CONSENSYS TO CONTROL.  YOU UNDERSTAND THAT + PROTOCOL UPGRADES MAY INADVERTENTLY CONTAIN BUGS OR SECURITY + VULNERABILITIES THAT MAY RESULT IN LOSS OF FUNCTIONALITY AND + ULTIMATELY FUNDS. + + + YOU UNDERSTAND AND ACCEPT THAT CONSENSYS DOES NOT CONTROL ANY + BLOCKCHAIN PROTOCOL, NOR DOES CONSENSYS CONTROL ANY SMART CONTRACT + THAT IS NOT OTHERWISE OFFERED BY CONSENSYS AS PART OF THE + OFFERINGS.  YOU UNDERSTAND AND ACCEPT THAT CONSENSYS DOES NOT + CONTROL AND IS NOT RESPONSIBLE FOR THE TRANSITION OF ANY BLOCKCHAIN + PROTOCOL FROM PROOF OF WORK TO PROOF OF STAKE CONSENSUS.  YOU + AGREE THAT YOU ALONE, AND NOT CONSENSYS, IS RESPONSIBLE FOR ANY + TRANSACTIONS THAT YOU ENGAGE IN WITH REGARD TO SUPPORTING ANY + BLOCKCHAIN PROTOCOL WHETHER THROUGH TRANSACTION VALIDATION OR + OTHERWISE, OR ANY TRANSACTIONS THAT YOU ENGAGE IN WITHANY + THIRD-PARTY-DEVELOPED SMART CONTRACT OR TOKEN, INCLUDING TOKENS THAT + WERE CREATED BY A THIRD PARTY FOR THE PURPOSE OF FRAUDULENTLY + MISREPRESENTING AFFILIATION WITH ANY BLOCKCHAIN PROJECT.  YOU + AGREE THAT CONSENSYS IS NOT RESPONSIBLE FOR THE REGULATORY STATUS OR + TREATMENT OF ANY DIGITAL ASSETS THAT YOU MAY ACCESS OR TRANSACT WITH + USING CONSENSYS OFFERINGS.  YOU EXPRESSLY ASSUME FULL + RESPONSIBILITY FOR ALL OF THE RISKS OF ACCESSING AND USING THE + OFFERINGS TO INTERACT WITH BLOCKCHAIN PROTOCOLS.  + + + 10. Limitations of Liability. + + + 10.1 Limitation of Liability. WITH THE EXCEPTION OF CLAIMS RELATING + TO A BREACH OF OUR PROPRIETARY RIGHTS AS GOVERNED BY SECTION 7 AND + INTELLECTUAL PROPERTY CLAIMS AS GOVERNED BY SECTION 8, IN NO EVENT + SHALL THE AGGREGATE LIABILITY OF EACH PARTY TOGETHER WITH ALL OF ITS + AFFILIATES ARISING OUT OF OR RELATED TO THIS AGREEMENT EXCEED THE + TOTAL AMOUNT PAID BY YOU HEREUNDER FOR THE OFFERINGS GIVING RISE TO + THE LIABILITY IN THE TWELVE MONTHS PRECEDING THE FIRST INCIDENT OUT + OF WHICH THE LIABILITY AROSE, OR, IF NO FEES HAVE BEEN PAID, + $25,000. THE FOREGOING LIMITATION WILL APPLY WHETHER AN ACTION IS IN + CONTRACT OR TORT AND REGARDLESS OF THE THEORY OF LIABILITY, BUT WILL + NOT LIMIT YOUR PAYMENT OBLIGATIONS UNDER SECTION 4.  + + + 10.2 Exclusion of Consequential and Related Damages. IN NO EVENT + WILL EITHER PARTY OR ITS AFFILIATES HAVE ANY LIABILITY ARISING OUT + OF OR RELATED TO THIS AGREEMENT FOR ANY LOST PROFITS, REVENUES, + GOODWILL, OR INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, COVER, + BUSINESS INTERRUPTION OR PUNITIVE DAMAGES, WHETHER AN ACTION IS IN + CONTRACT OR TORT AND REGARDLESS OF THE THEORY OF LIABILITY, EVEN IF + A PARTY OR ITS AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF + SUCH DAMAGES OR IF A PARTY’S OR ITS AFFILIATES’ REMEDY OTHERWISE + FAILS OF ITS ESSENTIAL PURPOSE. THE FOREGOING DISCLAIMER WILL NOT + APPLY TO THE EXTENT PROHIBITED BY LAW. + + + 11. Binding Arbitration and Class Action Waiver. + + + PLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT + YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT. + + + 11.1 Binding Arbitration. Any dispute, claim or controversy + (“Claim”) relating in any way to this Agreement, the Site, or your + use of the Offerings will be resolved by binding arbitration as + provided in this Section 11, rather than in court, except that you + may assert claims in small claims court if your claims qualify. + + + 11.1.1 If you are located in the United States: This agreement and + any dispute or claim (including non-contractual disputes or claims) + arising out of or in connection with it or its subject matter or + formation shall be governed by and construed in accordance with the + laws of the State of New York. The Federal Arbitration Act and + federal arbitration law apply to this Agreement. There is no judge + or jury in arbitration, and court review of an arbitration award is + limited. However, an arbitrator can award on an individual basis the + same damages and relief as a court (including injunctive and + declaratory relief or statutory damages), and must follow the terms + of this Agreement as a court would. The arbitration will be + conducted in accordance with the expedited procedures set forth in + the JAMS Comprehensive Arbitration Rules and Procedures (the + “Rules”) as those Rules exist on the effective date of this + Agreement, including Rules 16.1 and 16.2 of those Rules. The + arbitrator’s decision shall be final, binding, and non-appealable. + Judgment upon the award may be entered and enforced in any court + having jurisdiction. Neither party shall sue the other party other + than as provided herein or for enforcement of this clause or of the + arbitrator’s award; any such suit may be brought only in a Federal + District Court or a New York state court located in New York County, + New York. The arbitrator, and not any federal, state, or local + court, shall have exclusive authority to resolve any dispute + relating to the interpretation, applicability, unconscionability, + arbitrability, enforceability, or formation of this Agreement + including any claim that all or any part of the Agreement is void or + voidable.  If for any reason a claim proceeds in court rather + than in arbitration we and you waive any right to a jury trial. + Notwithstanding the foregoing we and you both agree that you or we + may bring suit in court to enjoin infringement or other misuse of + intellectual property rights.  + + + 11.1.2 If you are located in the United Kingdom: This agreement and + any dispute or claim (including non-contractual disputes or claims) + arising out of or in connection with it or its subject matter or + formation shall be governed by and construed in accordance with the + law of England and Wales. Any dispute, claim or controversy relating + in any way to this Agreement, the Offerings, your use of the + Offerings, or to any products or services licensed or distributed by + us will be resolved by binding arbitration as provided in this + clause. Prior to commencing any formal arbitration proceedings, + parties shall first seek settlement of any claim by mediation in + accordance with the LCIA Mediation Rules, which Rules are deemed to + be incorporated by reference into this clause. If the dispute is not + settled by mediation within 14 days of the commencement of the + mediation, or such further period as the parties shall agree in + writing, the dispute shall be referred to and finally resolved by + arbitration under the LCIA Rules, which are deemed to be + incorporated by reference into this clause. The language to be used + in the mediation and in the arbitration shall be English. The seat + or legal place of arbitration shall be London. + + + 11.1.3 If you are located in any territory that is not specifically + enumerated in Sections 11.1.1 or 11.1.2, you may elect for either of + Section 11.1.1 or 11.1.2 to apply to you, otherwise this Agreement + and any Claim (including non-contractual disputes or claims) arising + out of or in connection with it or its subject matter or formation + shall be governed by and construed in accordance with the law of + Ireland. Any Claim relating in any way to this Agreement, the + Offerings, your use of the Offerings, or to any products or services + licensed or distributed by us will be resolved by binding + arbitration as provided in this clause. Prior to commencing any + formal arbitration proceedings, parties shall first seek settlement + of any claim by mediation in accordance with the LCIA Mediation + Rules, which Rules are deemed to be incorporated by reference into + this clause. If the dispute is not settled by mediation within 14 + days of the commencement of the mediation, or such further period as + the parties shall agree in writing, the Claim shall be referred to + and finally resolved by arbitration under the LCIA Rules, which are + deemed to be incorporated by reference into this clause. The + language to be used in the mediation and in the arbitration shall be + English. The seat or legal place of arbitration shall be Dublin, + Ireland. + + + 11.2 Class Action Waiver. YOU AND WE AGREE THAT EACH MAY BRING + CLAIMS AGAINST THE OTHER ONLY ON AN INDIVIDUAL BASIS, AND NOT AS A + PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE + PROCEEDING. YOU AND WE EXPRESSLY WAIVE ANY RIGHT TO FILE A CLASS + ACTION OR SEEK RELIEF ON A CLASS BASIS. Unless both you and we + agree, no arbitrator or judge may consolidate more than one person’s + claims or otherwise preside over any form of a representative or + class proceeding. The arbitrator may award injunctive relief only in + favor of the individual party seeking relief and only to the extent + necessary to provide relief warranted by that party’s individual + claim. If a court decides that applicable law precludes enforcement + of any of this paragraph’s limitations as to a particular claim for + relief, then that claim (and only that claim) must be severed from + the arbitration and may be brought in court. If any court or + arbitrator determines that the class action waiver set forth in this + paragraph is void or unenforceable for any reason or that an + arbitration can proceed on a class basis, then the arbitration + provision set forth above shall be deemed null and void in its + entirety and the parties shall be deemed to have not agreed to + arbitrate disputes. + + + 11.3 30-Day Right to Opt Out. You have the right to opt-out and not + be bound by the arbitration and class action waiver provisions set + forth above by sending written notice of your decision to opt-out to + the email address notices@consensys.net with subject line LEGAL OPT + OUT. The notice must be sent within 30 days of your first use of the + Offerings, otherwise you shall be bound to arbitrate disputes and + will be deemed to have agreed to waive any right to pursue a class + action in accordance with the terms of those paragraphs. If you + opt-out of these provisions, we will also not be bound by them. + + + 12. Miscellaneous. + + + 12.1 Assignment. You will not assign or otherwise transfer this + Agreement or any of your rights and obligations under this + Agreement, without our prior written consent. Any assignment or + transfer in violation of this Section 12.1 will be void. We may + assign this Agreement without your consent (a) in connection with a + merger, acquisition or sale of all or substantially all of our + assets, or (b) to any Affiliate or as part of a corporate + reorganization; and effective upon such assignment, the assignee is + deemed substituted for us as a party to this Agreement and we are + fully released from all of our obligations and duties to perform + under this Agreement. Subject to the foregoing, this Agreement will + be binding upon, and inure to the benefit of the parties and their + respective permitted successors and assigns. + + + 12.2 DAOs. As a blockchain native company, we may interact with and + provide certain Offerings to DAOs. Due to the unique nature of DAOs, + to the extent the DAO votes in favor of and/or accepts such + Offerings from ConsenSys, the DAO has acknowledged and agreed to + these Terms in their entirety. + + + 12.2 Entire Agreement and Modifications. This Agreement incorporates + the Policies by reference and is the entire agreement between you + and us regarding the subject matter of this Agreement. If the terms + of this document are inconsistent with the terms contained in any + Policy, the terms contained in this document will control. Any + modification to the terms of this Agreement may only be made in + writing. + + + 12.3 Force Majeure. Neither party nor their respective affiliates + will be liable for any delay or failure to perform any obligation + under this Agreement where the delay or failure results from any + cause beyond such party’s reasonable control, including but not + limited to acts of God, utilities or other telecommunications + failures, cyber attacks, earthquake, storms or other elements of + nature, pandemics, blockages, embargoes, riots, acts or orders of + government, acts of terrorism, or war. + + + 12.4 Export and Sanctions Compliance. In connection with this + Agreement, you will comply with all applicable import, re-import, + sanctions, anti-boycott, export, and re-export control laws and + regulations, including all such laws and regulations that may apply. + For clarity, you are solely responsible for compliance related to + the manner in which you choose to use the Offerings. You may not use + any Offering if you are the subject of U.S. sanctions or of + sanctions consistent with U.S. law imposed by the governments of the + country where you are using the Offering.  + + + 12.5 Independent Contractors; Non-Exclusive Rights. We and you are + independent contractors, and this Agreement will not be construed to + create a partnership, joint venture, agency, or employment + relationship. Neither party, nor any of their respective affiliates, + is an agent of the other for any purpose or has the authority to + bind the other. Both parties reserve the right (a) to develop or + have developed for it products, services, concepts, systems, or + techniques that are similar to or compete with the products, + services, concepts, systems, or techniques developed or contemplated + by the other party, and (b) to assist third party developers or + systems integrators who may offer products or services which compete + with the other party’s products or services. + + + 12.6 Eligibility. If you are under the age of majority in your + jurisdiction of residence, you may use the Site or Offerings only + with the consent of or under the supervision of your parent or legal + guardian. + + + NOTICE TO PARENTS AND GUARDIANS: By granting your minor permission + to access the Site or Offerings, you agree to these Terms of Use on + behalf of your minor. You are responsible for exercising supervision + over your minor’s online activities. If you do not agree to these + Terms of Use, do not let your minor use the Site or Offerings. + + + 12.7 Language. All communications and notices made or given pursuant + to this Agreement must be in the English language. If we provide a + translation of the English language version of this Agreement, the + English language version of the Agreement will control if there is + any conflict. + + + 12.8 Notice. + + + (a) To You. We may provide any notice to you under this Agreement + using commercially reasonable means, including: (i) posting a notice + on the Site; (ii) sending a message to the email address then + associated with your account; or (iii) using public communication + channels . Notices we provide by posting on the Site or using public + communication channels will be effective upon posting, and notices + we provide by email will be effective when we send the email. It is + your responsibility to keep your email address current to the extent + you have an account. You will be deemed to have received any email + sent to the email address then associated with your account when we + send the email, whether or not you actually receive the email. + + + (b) To Us. To give us notice under this Agreement, you must contact + us by email at notices@consensys.net.  + + + 12.9 No Third-Party Beneficiaries. Except as otherwise set forth + herein, this Agreement does not create any third-party beneficiary + rights in any individual or entity that is not a party to this + Agreement. + + + 12.10 No Waivers. The failure by us to enforce any provision of this + Agreement will not constitute a present or future waiver of such + provision nor limit our right to enforce such provision at a later + time. All waivers by us must be in writing to be effective. + + + 12.11 Severability. If any portion of this Agreement is held to be + invalid or unenforceable, the remaining portions of this Agreement + will remain in full force and effect. Any invalid or unenforceable + portions will be interpreted to effect and intent of the original + portion. If such construction is not possible, the invalid or + unenforceable portion will be severed from this Agreement but the + rest of the Agreement will remain in full force and effect. + + + 12.12 Notice and Procedure for Making Claims of Copyright + Infringement. If you are a copyright owner or agent of the owner, + and you believe that your copyright or the copyright of a person on + whose behalf you are authorized to act has been infringed, please + provide us a written notice at the address below with the following + information: + + + + an electronic or physical signature of the person authorized to + act on behalf of the owner of the copyright or other intellectual + property interest; + + + a description of the copyrighted work or other intellectual + property that you claim has been infringed; + + + a description of where the material that you claim is infringing + is located with respect to the Offerings; + + + your address, telephone number, and email address; + + + a statement by you that you have a good faith belief that the + disputed use is not authorized by the copyright owner, its agent, + or the law; + + + a statement by you, made under penalty of perjury, that the above + information in your notice is accurate and that you are the + copyright or intellectual property owner or authorized to act on + the copyright or intellectual property owner’s behalf. + + + + You can reach us at: + + + Email: notices@consensys.net + + + Subject Line: Copyright Notification Mail + + + Attention: Copyright ℅ + + + ConsenSys Software Inc.  + + + 49 Bogart Street Suite 22 Brooklyn, NY 11206 + + + 13. Definitions. + + + “Acceptable Use Policy” means the policy set forth below, as it may + be updated by us from time to time. You agree not to, and not to + allow third parties to, use the Offerings: + + + + to violate, or encourage the violation of, the legal rights of + others (for example, this may include allowing End Users to + infringe or misappropriate the intellectual property rights of + others in violation of the Digital Millennium Copyright Act); + + + to engage in, promote or encourage any illegal or infringing + content; + + + for any unlawful, invasive, infringing, defamatory or fraudulent + purpose (for example, this may include phishing, creating a + pyramid scheme or mirroring a website); + + + to intentionally distribute viruses, worms, Trojan horses, + corrupted files, hoaxes, or other items of a destructive or + deceptive nature; + + + to interfere with the use of the Offerings, or the equipment used + to provide the Offerings, by customers, authorized resellers, or + other authorized users; + + + to disable, interfere with or circumvent any aspect of the + Offerings (for example, any thresholds or limits); + + + to generate, distribute, publish or facilitate unsolicited mass + email, promotions, advertising or other solicitation; or + + + to use the Offerings, or any interfaces provided with the + Offerings, to access any other product or service in a manner that + violates the terms of service of such other product or service. + + + + “API” means an application program interface. + + + “API Requests” has the meaning set forth in Section 5.3. + + + “Applicable Threshold” has the meaning set forth in Section 4.2. + + + “Base Fees” has the meaning set forth in Section 4.2. + + + “Content” means any data, text, audio, video or images, software + (including machine images), and any documentation. + + + “DAO” means Decentralized Autonomous Organization. + + + “End User” means any individual or entity that directly or + indirectly through another user: (a) accesses or uses Your Content; + or (b) otherwise accesses or uses the Offerings under your + account.  + + + “Fees” has the meaning set forth in Section 4.2. + + + “Losses” means any claims, damages, losses, liabilities, costs, and + expenses (including reasonable attorneys’ fees).’ + + + “Our Content” means any software (including machine images), data, + text, audio, video, images, or documentation that we offer in + connection with the Offerings.  + + + “Our Marks” means any trademarks, service marks, service or trade + names, logos, and other designations of ConsenSys Software Inc. and + their affiliates or licensors that we may make available to you in + connection with this Agreement. + + + “Order” means an order for Offerings executed through an order form + directly with ConsenSys, or through a cloud vendor, such as Amazon + Web Services, Microsoft Azure, or Google Cloud. + + + “Offerings” means each of the products and services, including but + not limited to Codefi, Infura, MetaMask, Quorum and any other + features, tools, materials, or services offered from time to time, + by us or our affiliates.  + + + “Policies” means the Acceptable Use Policy, Privacy Policy, any + supplemental policies or addendums applicable to any Service as + provided to you, and any other policy or terms referenced in or + incorporated into this Agreement, each as may be updated by us from + time to time. + + + “Privacy Policy” means the privacy policy located at{' '} + + https://consensys.net/privacy-policy + {' '} + (and any successor or related locations designated by us), as it may + be updated by us from time to time. + + + “Service Offerings” means the Services (including associated APIs), + Our Content, Our Marks, and any other product or service provided by + us under this Agreement. Service Offerings do not include + Third-Party Content or Third-Party Services. + + + “Suggestions” means all suggested improvements to the Service + Offerings that you provide to us.. + + + “Term” means the term of this Agreement described in Section 6.1. + + + “Termination Date” means the effective date of termination provided + in accordance with Section 6, in a notice from one party to the + other. + + + “Third-Party Content” means Content made available to you by any + third party on the Site or in conjunction with the Offerings. + + + “Your Content” means content that you or any End User transfers to + us, storage or hosting by the Offerings in connection with account + and any computational results that you or any End User derive from + the foregoing through their use of the Offerings, excluding however + any information submitted to a blockchain protocol for + processing.  + + + { + setIsTermsOfUseChecked(!isTermsOfUseChecked); + }} + /> + + + + + + ); +} + +TermsOfUsePopup.propTypes = { + onAccept: PropTypes.func.isRequired, +}; diff --git a/ui/components/app/terms-of-use-popup/terms-of-use-popup.stories.js b/ui/components/app/terms-of-use-popup/terms-of-use-popup.stories.js new file mode 100644 index 000000000000..18272ab61446 --- /dev/null +++ b/ui/components/app/terms-of-use-popup/terms-of-use-popup.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; +import TermsOfUsePopup from '.'; + +export default { + title: 'Components/App/TermsOfUsePopup', + component: TermsOfUsePopup, + argTypes: { + onAccept: { + action: 'onAccept', + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/terms-of-use-popup/terms-of-use-popup.test.js b/ui/components/app/terms-of-use-popup/terms-of-use-popup.test.js new file mode 100644 index 000000000000..02125e981ccd --- /dev/null +++ b/ui/components/app/terms-of-use-popup/terms-of-use-popup.test.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import TermsOfUsePopup from './terms-of-use-popup'; + +const render = () => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + const onAccept = jest.fn(); + return renderWithProvider(, store); +}; + +describe('TermsOfUsePopup', () => { + beforeEach(() => { + const mockIntersectionObserver = jest.fn(); + mockIntersectionObserver.mockReturnValue({ + observe: () => null, + unobserve: () => null, + disconnect: () => null, + }); + window.IntersectionObserver = mockIntersectionObserver; + }); + + it('renders TermsOfUse component and shows Terms of Use text', () => { + render(); + expect( + screen.getByText('Our Terms of Use have updated'), + ).toBeInTheDocument(); + }); + + it('scrolls down when handleScrollDownClick is called', () => { + render(); + const mockScrollIntoView = jest.fn(); + window.HTMLElement.prototype.scrollIntoView = mockScrollIntoView; + const button = document.querySelector( + "[data-testid='popover-scroll-button']", + ); + fireEvent.click(button); + expect(mockScrollIntoView).toHaveBeenCalledWith({ + behavior: 'smooth', + }); + }); +}); diff --git a/ui/components/app/wallet-overview/wallet-overview.js b/ui/components/app/wallet-overview/wallet-overview.js index 9265e31b08ea..5be0ef73b537 100644 --- a/ui/components/app/wallet-overview/wallet-overview.js +++ b/ui/components/app/wallet-overview/wallet-overview.js @@ -1,12 +1,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import { useSelector } from 'react-redux'; +import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { getSelectedIdentity } from '../../../selectors'; +import { AddressCopyButton } from '../../multichain'; const WalletOverview = ({ balance, buttons, className, icon, loading }) => { + const selectedIdentity = useSelector(getSelectedIdentity); + const checksummedAddress = toChecksumHexAddress(selectedIdentity?.address); return (
- {loading ? null : icon} + {process.env.MULTICHAIN ? ( + + ) : ( + <>{loading ? null : icon} + )} {balance}
{buttons}
diff --git a/ui/components/app/whats-new-popup/index.scss b/ui/components/app/whats-new-popup/index.scss index 04c59af1f3de..788eb651f2e7 100644 --- a/ui/components/app/whats-new-popup/index.scss +++ b/ui/components/app/whats-new-popup/index.scss @@ -66,28 +66,6 @@ height: 1px; width: 100%; } - - &__scroll-button { - position: absolute; - bottom: 12px; - right: 12px; - height: 32px; - width: 32px; - border-radius: 14px; - border: 1px solid var(--color-border-default); - background: var(--color-background-alternative); - color: var(--color-icon-default); - z-index: 201; - cursor: pointer; - opacity: 0.8; - display: flex; - justify-content: center; - align-items: center; - - &:hover { - opacity: 1; - } - } } .popover-wrap.whats-new-popup__popover { diff --git a/ui/components/institutional/interactive-replacement-token-modal/index.js b/ui/components/institutional/interactive-replacement-token-modal/index.js new file mode 100644 index 000000000000..1f1b5f7f68b8 --- /dev/null +++ b/ui/components/institutional/interactive-replacement-token-modal/index.js @@ -0,0 +1 @@ +export { default } from './interactive-replacement-token-modal'; diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js new file mode 100644 index 000000000000..0f40723d4a4d --- /dev/null +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.js @@ -0,0 +1,154 @@ +import React, { useContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Modal from '../../app/modal'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { hideModal } from '../../../store/actions'; +import { getSelectedAddress } from '../../../selectors/selectors'; +import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { Text } from '../../component-library'; +import Box from '../../ui/box'; +import { + BLOCK_SIZES, + BackgroundColor, + DISPLAY, + FLEX_WRAP, + FLEX_DIRECTION, + BorderRadius, + FONT_WEIGHT, + TEXT_ALIGN, + AlignItems, +} from '../../../helpers/constants/design-system'; + +const InteractiveReplacementTokenModal = () => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const dispatch = useDispatch(); + + const { url } = useSelector( + (state) => state.metamask.interactiveReplacementToken || {}, + ); + + const { custodians } = useSelector( + (state) => state.metamask.mmiConfiguration, + ); + const address = useSelector(getSelectedAddress); + const custodyAccountDetails = useSelector( + (state) => + state.metamask.custodyAccountDetails[toChecksumHexAddress(address)], + ); + + const custodianName = custodyAccountDetails?.custodianName; + const custodian = + custodians.find((item) => item.name === custodianName) || {}; + + const renderCustodyInfo = () => { + let img; + + if (custodian.iconUrl) { + img = ( + + + {custodian.displayName} + + + ); + } else { + img = ( + + {custodian.displayName} + + ); + } + + return ( + <> + {img} + + {t('custodyRefreshTokenModalTitle')} + + + {t('custodyRefreshTokenModalDescription', [custodian.displayName])} + + + {t('custodyRefreshTokenModalSubtitle')} + + + {t('custodyRefreshTokenModalDescription1')} + + + {t('custodyRefreshTokenModalDescription2')} + + + ); + }; + + const handleSubmit = () => { + global.platform.openTab({ + url, + }); + + trackEvent({ + category: 'MMI', + event: 'User clicked refresh token link', + }); + }; + + const handleClose = () => { + dispatch(hideModal()); + }; + + return ( + + + {renderCustodyInfo(custodian)} + + + ); +}; + +export default InteractiveReplacementTokenModal; diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js new file mode 100644 index 000000000000..86c03504c103 --- /dev/null +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.stories.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import InteractiveReplacementTokenModal from '.'; + +const customData = { + ...testData, + metamask: { + ...testData.metamask, + mmiConfiguration: { + portfolio: { + enabled: true, + url: 'https://dev.metamask-institutional.io/', + }, + features: { + websocketApi: true, + }, + custodians: [ + { + refreshTokenUrl: + 'https://saturn-custody.dev.metamask-institutional.io/oauth/token', + name: 'saturn-dev', + displayName: 'Saturn Custody', + enabled: true, + mmiApiUrl: 'https://api.dev.metamask-institutional.io/v1', + websocketApiUrl: + 'wss://websocket.dev.metamask-institutional.io/v1/ws', + apiBaseUrl: + 'https://saturn-custody.dev.metamask-institutional.io/eth', + iconUrl: + 'https://saturn-custody-ui.dev.metamask-institutional.io/saturn.svg', + isNoteToTraderSupported: true, + }, + ], + }, + custodyAccountDetails: { + '0xAddress': { + address: '0xAddress', + details: 'details', + custodyType: 'testCustody - Saturn', + custodianName: 'saturn-dev', + }, + }, + provider: { + type: 'test', + }, + selectedAddress: '0xAddress', + isUnlocked: true, + interactiveReplacementToken: { + oldRefreshToken: 'abc', + url: 'https://saturn-custody-ui.dev.metamask-institutional.io', + }, + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, +}; + +const store = configureStore(customData); + +export default { + title: 'Components/Institutional/InteractiveReplacementToken-Modal', + decorators: [(story) => {story()}], + component: InteractiveReplacementTokenModal, +}; + +export const DefaultStory = (args) => ( + +); + +DefaultStory.storyName = 'InteractiveReplacementTokenModal'; diff --git a/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js new file mode 100644 index 000000000000..4a78a223716d --- /dev/null +++ b/ui/components/institutional/interactive-replacement-token-modal/interactive-replacement-token-modal.test.js @@ -0,0 +1,91 @@ +import React from 'react'; +import sinon from 'sinon'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import testData from '../../../../.storybook/test-data'; +import InteractiveReplacementTokenModal from '.'; + +describe('Interactive Replacement Token Modal', function () { + const mockStore = { + ...testData, + metamask: { + ...testData.metamask, + mmiConfiguration: { + portfolio: { + enabled: true, + url: 'https://dev.metamask-institutional.io/', + }, + features: { + websocketApi: true, + }, + custodians: [ + { + refreshTokenUrl: + 'https://saturn-custody.dev.metamask-institutional.io/oauth/token', + name: 'saturn-dev', + displayName: 'Saturn Custody', + enabled: true, + mmiApiUrl: 'https://api.dev.metamask-institutional.io/v1', + websocketApiUrl: + 'wss://websocket.dev.metamask-institutional.io/v1/ws', + apiBaseUrl: + 'https://saturn-custody.dev.metamask-institutional.io/eth', + iconUrl: + 'https://saturn-custody-ui.dev.metamask-institutional.io/saturn.svg', + isNoteToTraderSupported: true, + }, + ], + }, + custodyAccountDetails: { + '0xAddress': { + address: '0xAddress', + details: 'details', + custodyType: 'testCustody - Saturn', + custodianName: 'saturn-dev', + }, + }, + provider: { + type: 'test', + }, + selectedAddress: '0xAddress', + isUnlocked: true, + interactiveReplacementToken: { + oldRefreshToken: 'abc', + url: 'https://saturn-custody-ui.dev.metamask-institutional.io', + }, + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + }; + + const store = configureMockStore()(mockStore); + + it('should render the interactive-replacement-token-modal', () => { + const { getByText, getByTestId } = renderWithProvider( + , + store, + ); + + expect(getByTestId('interactive-replacement-token-modal')).toBeVisible(); + expect(getByText('Your custodian session has expired')).toBeInTheDocument(); + }); + + it('opens new tab on Open Codefi Compliance click', async () => { + global.platform = { openTab: sinon.spy() }; + + const { container } = renderWithProvider( + , + store, + ); + + const button = container.getElementsByClassName('btn-primary')[0]; + + fireEvent.click(button); + + await waitFor(() => { + expect(global.platform.openTab.calledOnce).toStrictEqual(true); + }); + }); +}); diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js index ac65bcd5b4bf..df3cece9a576 100644 --- a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js @@ -12,8 +12,7 @@ import { } from '../../../selectors'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; import { Menu, MenuItem } from '../../ui/menu'; -import { Text } from '../../component-library'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { Text, IconName } from '../../component-library'; import { MetaMetricsEventCategory, MetaMetricsEventLinkType, @@ -74,7 +73,7 @@ export const AccountListItemMenu = ({ : openBlockExplorer } subtitle={blockExplorerUrlSubTitle || null} - iconName={ICON_NAMES.EXPORT} + iconName={IconName.Export} data-testid="account-list-menu-open-explorer" > {t('viewOnExplorer')} @@ -92,7 +91,7 @@ export const AccountListItemMenu = ({ onClose(); closeMenu?.(); }} - iconName={ICON_NAMES.SCAN_BARCODE} + iconName={IconName.ScanBarcode} > {t('accountDetails')} @@ -108,7 +107,7 @@ export const AccountListItemMenu = ({ ); onClose(); }} - iconName={ICON_NAMES.TRASH} + iconName={IconName.Trash} > {t('removeAccount')} diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index deeddff19eed..05a33ea0aacf 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -15,12 +15,10 @@ import { AvatarFavicon, Tag, ButtonLink, + ButtonIcon, + IconName, + IconSize, } from '../../component-library'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; -import { - ICON_NAMES, - ICON_SIZES, -} from '../../component-library/icon/deprecated'; import { Color, TEXT_ALIGN, @@ -197,8 +195,8 @@ export const AccountListItem = ({
{ e.stopPropagation(); setAccountOptionsMenuOpen(true); diff --git a/ui/components/multichain/account-list-menu/account-list-menu.js b/ui/components/multichain/account-list-menu/account-list-menu.js index fcb7d1046f92..aea2e81c6ff0 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.js +++ b/ui/components/multichain/account-list-menu/account-list-menu.js @@ -5,6 +5,7 @@ import Fuse from 'fuse.js'; import { useDispatch, useSelector } from 'react-redux'; import Box from '../../ui/box/box'; import { ButtonLink, TextFieldSearch, Text } from '../../component-library'; +// TODO: Replace ICON_NAMES with IconName when ButtonBase/Buttons have been updated import { ICON_NAMES } from '../../component-library/icon/deprecated'; import { AccountListItem } from '..'; import { @@ -103,7 +104,6 @@ export const AccountListMenu = ({ onClose }) => { }, }); dispatch(setSelectedAccount(account.address)); - onClose(); }} identity={account} key={account.address} diff --git a/ui/components/multichain/account-picker/account-picker.js b/ui/components/multichain/account-picker/account-picker.js index 72b522893895..5701249a1c8d 100644 --- a/ui/components/multichain/account-picker/account-picker.js +++ b/ui/components/multichain/account-picker/account-picker.js @@ -5,9 +5,10 @@ import { Button, AvatarAccount, AvatarAccountVariant, + Icon, + IconName, Text, } from '../../component-library'; -import { ICON_NAMES, Icon } from '../../component-library/icon/deprecated'; import { AlignItems, BackgroundColor, @@ -47,7 +48,7 @@ export const AccountPicker = ({ address, name, onClick }) => { {name} diff --git a/ui/components/multichain/address-copy-button/address-copy-button.js b/ui/components/multichain/address-copy-button/address-copy-button.js index ec15b34977d5..c71f8ba3d99e 100644 --- a/ui/components/multichain/address-copy-button/address-copy-button.js +++ b/ui/components/multichain/address-copy-button/address-copy-button.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { ButtonBase } from '../../component-library'; +// TODO: Replace ICON_NAMES with IconName when ButtonBase/Buttons have been updated import { ICON_NAMES } from '../../component-library/icon/deprecated'; import { BackgroundColor, diff --git a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap new file mode 100644 index 000000000000..d4d1cc8da2bb --- /dev/null +++ b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap @@ -0,0 +1,224 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`App Header should match snapshot 1`] = ` +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+`; diff --git a/ui/components/multichain/app-header/app-header.js b/ui/components/multichain/app-header/app-header.js new file mode 100644 index 000000000000..61662c503f61 --- /dev/null +++ b/ui/components/multichain/app-header/app-header.js @@ -0,0 +1,220 @@ +import React, { useContext, useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import browser from 'webextension-polyfill'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { + CONNECTED_ACCOUNTS_ROUTE, + DEFAULT_ROUTE, +} from '../../../helpers/constants/routes'; + +import { + AlignItems, + BackgroundColor, + BLOCK_SIZES, + DISPLAY, + JustifyContent, + Size, +} from '../../../helpers/constants/design-system'; +import { + AvatarNetwork, + Button, + ButtonIcon, + IconName, + PickerNetwork, +} from '../../component-library'; + +import { + getCurrentNetwork, + getOriginOfCurrentTab, + getSelectedIdentity, +} from '../../../selectors'; +import { GlobalMenu, AccountPicker } from '..'; + +import Box from '../../ui/box/box'; +import { toggleAccountMenu, toggleNetworkMenu } from '../../../store/actions'; +import MetafoxLogo from '../../ui/metafox-logo'; +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; +import ConnectedStatusIndicator from '../../app/connected-status-indicator'; + +export const AppHeader = ({ onClick }) => { + const trackEvent = useContext(MetaMetricsContext); + const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); + const menuRef = useRef(false); + const origin = useSelector(getOriginOfCurrentTab); + const history = useHistory(); + const isUnlocked = useSelector((state) => state.metamask.isUnlocked); + + // Used for account picker + const identity = useSelector(getSelectedIdentity); + const dispatch = useDispatch(); + + // Used for network icon / dropdown + const currentNetwork = useSelector(getCurrentNetwork); + + // used to get the environment and connection status + const popupStatus = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP; + const showStatus = + getEnvironmentType() === ENVIRONMENT_TYPE_POPUP && + origin && + origin !== browser.runtime.id; + + return ( + <> + {isUnlocked && !popupStatus ? ( + + { + if (onClick) { + await onClick(); + } + history.push(DEFAULT_ROUTE); + }} + /> + + ) : null} + + <> + {isUnlocked ? ( + + {popupStatus ? ( + + ) : ( + dispatch(toggleNetworkMenu())} + /> + )} + + dispatch(toggleAccountMenu())} + /> + + {showStatus ? ( + history.push(CONNECTED_ACCOUNTS_ROUTE)} + /> + ) : null} + + { + trackEvent({ + event: MetaMetricsEventName.NavAccountMenuOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + location: 'Home', + }, + }); + setAccountOptionsMenuOpen(true); + }} + /> + + + {accountOptionsMenuOpen ? ( + setAccountOptionsMenuOpen(false)} + /> + ) : null} + + ) : ( + + dispatch(toggleNetworkMenu())} + /> + { + if (onClick) { + await onClick(); + } + history.push(DEFAULT_ROUTE); + }} + /> + + )} + + + + ); +}; + +AppHeader.propTypes = { + /** + * The onClick handler to be passed to the MetaMask Logo in the App Header + */ + onClick: PropTypes.func, +}; diff --git a/ui/components/multichain/app-header/app-header.scss b/ui/components/multichain/app-header/app-header.scss new file mode 100644 index 000000000000..7ab95825a553 --- /dev/null +++ b/ui/components/multichain/app-header/app-header.scss @@ -0,0 +1,74 @@ +.multichain-app-header { + $height-screen-sm-max: 100%; + $width-screen-sm-min: 85vw; + $width-screen-md-min: 80vw; + $width-screen-lg-min: 62vw; + + flex-flow: column nowrap; + z-index: 55; + min-height: 64px; + + &__contents { + display: grid; + grid-template-columns: 1fr 2fr 1fr; + height: 64px; + + @include screen-sm-max { + height: $height-screen-sm-max; + } + + @include screen-sm-min { + width: $width-screen-sm-min; + } + + @include screen-md-min { + width: $width-screen-md-min; + } + + @include screen-lg-min { + width: $width-screen-lg-min; + } + + &--avatar-network { + background-color: transparent; + width: min-content; + padding: 8px; + + &:hover, + &:active { + box-shadow: none; + background: transparent; + } + } + } + + &__lock-contents { + flex-flow: row nowrap; + height: 64px; + + @include screen-sm-max { + height: $height-screen-sm-max; + } + + @include screen-sm-min { + width: $width-screen-sm-min; + } + + @include screen-md-min { + width: $width-screen-md-min; + } + + @include screen-lg-min { + width: $width-screen-lg-min; + } + } +} + +.multichain-app-header-shadow { + box-shadow: var(--shadow-size-md) var(--color-shadow-default); +} + +.multichain-app-header-logo { + height: 75px; + flex: 0 0 auto; +} diff --git a/ui/components/multichain/app-header/app-header.stories.js b/ui/components/multichain/app-header/app-header.stories.js new file mode 100644 index 000000000000..c8ae6a8103a4 --- /dev/null +++ b/ui/components/multichain/app-header/app-header.stories.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import testData from '../../../../.storybook/test-data'; +import { AppHeader } from '.'; + +const store = configureStore(testData); + +export default { + title: 'Components/Multichain/AppHeader', + decorators: [(story) => {story()}], + component: AppHeader, + argTypes: { + onClick: { + action: 'onClick', + }, + }, +}; +const customNetworkUnlockedData = { + ...testData, + metamask: { + ...testData.metamask, + preferences: { + showTestNetworks: true, + }, + isUnlocked: true, + networkConfigurations: { + ...testData.metamask.networkConfigurations, + }, + }, +}; +const customNetworkUnlockedStore = configureStore(customNetworkUnlockedData); + +const customNetworkLockedData = { + ...testData, + metamask: { + ...testData.metamask, + preferences: { + showTestNetworks: true, + }, + isUnlocked: false, + networkConfigurations: { + ...testData.metamask.networkConfigurations, + }, + }, +}; +const customNetworkLockedStore = configureStore(customNetworkLockedData); + +const Template = (args) => { + return ; +}; + +export const FullScreenAndUnlockedStory = Template.bind({}); + +FullScreenAndUnlockedStory.decorators = [ + (Story) => ( + + + + ), +]; + +export const FullScreenAndLockedStory = Template.bind({}); + +FullScreenAndLockedStory.decorators = [ + (Story) => ( + + + + ), +]; diff --git a/ui/components/multichain/app-header/app-header.test.js b/ui/components/multichain/app-header/app-header.test.js new file mode 100644 index 000000000000..c69caf12f84c --- /dev/null +++ b/ui/components/multichain/app-header/app-header.test.js @@ -0,0 +1,107 @@ +import React from 'react'; +import configureStore from 'redux-mock-store'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { AppHeader } from '.'; + +describe('App Header', () => { + it('should match snapshot', () => { + const mockState = { + activeTab: { + title: 'Eth Sign Tests', + origin: 'https://remix.ethereum.org', + protocol: 'https:', + url: 'https://remix.ethereum.org/', + }, + metamask: { + provider: { + chainId: CHAIN_IDS.GOERLI, + }, + accounts: { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': { + address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + }, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { + address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + }, + }, + preferences: { + showTestNetworks: true, + }, + cachedBalances: {}, + subjects: { + 'https://remix.ethereum.org': { + permissions: { + eth_accounts: { + caveats: [ + { + type: 'restrictReturnedAccounts', + value: [ + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + ], + }, + ], + date: 1586359844177, + id: '3aa65a8b-3bcb-4944-941b-1baa5fe0ed8b', + invoker: 'https://remix.ethereum.org', + parentCapability: 'eth_accounts', + }, + }, + }, + 'peepeth.com': { + permissions: { + eth_accounts: { + caveats: [ + { + type: 'restrictReturnedAccounts', + value: ['0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'], + }, + ], + date: 1585676177970, + id: '840d72a0-925f-449f-830a-1aa1dd5ce151', + invoker: 'peepeth.com', + parentCapability: 'eth_accounts', + }, + }, + }, + }, + identities: { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': { + address: '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + name: 'Really Long Name That Should Be Truncated', + }, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { + address: '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + lastSelected: 1586359844192, + name: 'Account 1', + }, + }, + keyrings: [ + { + accounts: [ + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5', + '0x7250739de134d33ec7ab1ee592711e15098c9d2d', + ], + }, + ], + permissionHistory: { + 'https://remix.ethereum.org': { + eth_accounts: { + accounts: { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': 1586359844192, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': 1586359844192, + }, + lastApproved: 1586359844192, + }, + }, + }, + }, + }; + + const mockStore = configureStore(); + const store = mockStore(mockState); + const { container } = renderWithProvider(, store); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/components/multichain/app-header/index.js b/ui/components/multichain/app-header/index.js new file mode 100644 index 000000000000..1126d287c99e --- /dev/null +++ b/ui/components/multichain/app-header/index.js @@ -0,0 +1 @@ +export { AppHeader } from './app-header'; diff --git a/ui/components/multichain/global-menu/global-menu.js b/ui/components/multichain/global-menu/global-menu.js index 7500c3046d87..2d89cf688bc4 100644 --- a/ui/components/multichain/global-menu/global-menu.js +++ b/ui/components/multichain/global-menu/global-menu.js @@ -9,7 +9,7 @@ import { } from '../../../helpers/constants/routes'; import { lockMetamask } from '../../../store/actions'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { IconName } from '../../component-library'; import { Menu, MenuItem } from '../../ui/menu'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; @@ -31,7 +31,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { return ( { history.push(CONNECTED_ROUTE); trackEvent({ @@ -47,7 +47,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { {t('connectedSites')} { const portfolioUrl = process.env.PORTFOLIO_URL; global.platform.openTab({ @@ -75,7 +75,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { {getEnvironmentType() === ENVIRONMENT_TYPE_FULLSCREEN ? null : ( { global.platform.openExtensionInBrowser(); trackEvent({ @@ -93,7 +93,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { )} { global.platform.openTab({ url: SUPPORT_LINK }); trackEvent( @@ -117,7 +117,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { {t('support')} { history.push(SETTINGS_ROUTE); trackEvent({ @@ -133,7 +133,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { {t('settings')} { dispatch(lockMetamask()); history.push(DEFAULT_ROUTE); diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index a4703b15887c..c9cf2efe1c33 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -2,6 +2,7 @@ export { AccountListItem } from './account-list-item'; export { AccountListItemMenu } from './account-list-item-menu'; export { AccountListMenu } from './account-list-menu'; export { AccountPicker } from './account-picker'; +export { AppHeader } from './app-header'; export { DetectedTokensBanner } from './detected-token-banner'; export { GlobalMenu } from './global-menu'; export { MultichainImportTokenLink } from './multichain-import-token-link'; diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss index 63b19d690c5d..75b42ef2a813 100644 --- a/ui/components/multichain/multichain-components.scss +++ b/ui/components/multichain/multichain-components.scss @@ -8,6 +8,7 @@ @import 'account-list-item/index'; @import 'account-list-menu/index'; @import 'account-picker/index'; +@import 'app-header/app-header'; @import 'multichain-connected-site-menu/index'; @import 'account-list-menu/'; @import 'multichain-token-list-item/multichain-token-list-item'; diff --git a/ui/components/multichain/multichain-connected-site-menu/multichain-connected-site-menu.js b/ui/components/multichain/multichain-connected-site-menu/multichain-connected-site-menu.js index e0303dd0b6f2..abc22247e715 100644 --- a/ui/components/multichain/multichain-connected-site-menu/multichain-connected-site-menu.js +++ b/ui/components/multichain/multichain-connected-site-menu/multichain-connected-site-menu.js @@ -13,8 +13,7 @@ import { IconColor, Size, } from '../../../helpers/constants/design-system'; -import { BadgeWrapper } from '../../component-library'; -import { Icon, ICON_NAMES } from '../../component-library/icon/deprecated'; +import { BadgeWrapper, Icon, IconName } from '../../component-library'; import Box from '../../ui/box'; import { getSelectedIdentity } from '../../../selectors'; import Tooltip from '../../ui/tooltip'; @@ -69,7 +68,7 @@ export const MultichainConnectedSiteMenu = ({ } > diff --git a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js index d2ced700bf96..34ad99dbaac5 100644 --- a/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js +++ b/ui/components/multichain/multichain-import-token-link/multichain-import-token-link.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import Box from '../../ui/box/box'; import { ButtonLink } from '../../component-library'; +// TODO: Replace ICON_NAMES with IconName when ButtonBase/Buttons have been updated import { ICON_NAMES } from '../../component-library/icon/deprecated'; import { AlignItems, diff --git a/ui/components/multichain/network-list-item/network-list-item.js b/ui/components/multichain/network-list-item/network-list-item.js index 3dedbfdd3df5..d2183273261b 100644 --- a/ui/components/multichain/network-list-item/network-list-item.js +++ b/ui/components/multichain/network-list-item/network-list-item.js @@ -12,9 +12,12 @@ import { TextColor, BLOCK_SIZES, } from '../../../helpers/constants/design-system'; -import { AvatarNetwork, ButtonLink } from '../../component-library'; -import { ButtonIcon } from '../../component-library/button-icon/deprecated'; -import { ICON_NAMES } from '../../component-library/icon/deprecated'; +import { + AvatarNetwork, + ButtonIcon, + ButtonLink, + IconName, +} from '../../component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; import Tooltip from '../../ui/tooltip/tooltip'; @@ -68,7 +71,7 @@ export const NetworkListItem = ({ { diff --git a/ui/components/multichain/network-list-menu/network-list-menu.js b/ui/components/multichain/network-list-menu/network-list-menu.js index 0cf1818ca69c..64a84f9daf20 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.js @@ -10,6 +10,7 @@ import { showModal, setShowTestNetworks, setProviderType, + toggleNetworkMenu, } from '../../../store/actions'; import { CHAIN_IDS, TEST_CHAINS } from '../../../../shared/constants/network'; import { @@ -30,7 +31,7 @@ import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; const UNREMOVABLE_CHAIN_IDS = [CHAIN_IDS.MAINNET, ...TEST_CHAINS]; -export const NetworkListMenu = ({ closeMenu }) => { +export const NetworkListMenu = ({ onClose }) => { const t = useI18nContext(); const networks = useSelector(getAllNetworks); const showTestNetworks = useSelector(getShowTestNetworks); @@ -42,7 +43,7 @@ export const NetworkListMenu = ({ closeMenu }) => { const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN; return ( - + <> {networks.map((network) => { @@ -58,16 +59,17 @@ export const NetworkListMenu = ({ closeMenu }) => { key={network.id || network.chainId} selected={isCurrentNetwork} onClick={() => { + dispatch(toggleNetworkMenu()); if (network.providerType) { dispatch(setProviderType(network.providerType)); } else { dispatch(setActiveNetwork(network.id)); } - closeMenu(); }} onDeleteClick={ canDeleteNetwork ? () => { + dispatch(toggleNetworkMenu()); dispatch( showModal({ name: 'CONFIRM_DELETE_NETWORK', @@ -75,7 +77,6 @@ export const NetworkListMenu = ({ closeMenu }) => { onConfirm: () => undefined, }), ); - closeMenu(); } : null } @@ -104,6 +105,7 @@ export const NetworkListMenu = ({ closeMenu }) => { : global.platform.openExtensionInBrowser( ADD_POPULAR_CUSTOM_NETWORK, ); + dispatch(toggleNetworkMenu()); }} > {t('addNetwork')} @@ -118,5 +120,5 @@ NetworkListMenu.propTypes = { /** * Executes when the menu should be closed */ - closeMenu: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, }; diff --git a/ui/components/multichain/network-list-menu/network-list-menu.stories.js b/ui/components/multichain/network-list-menu/network-list-menu.stories.js index 0629cd8e670e..f2c5fabfb497 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.stories.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.stories.js @@ -42,8 +42,8 @@ export default { title: 'Components/Multichain/NetworkListMenu', component: NetworkListMenu, argTypes: { - closeMenu: { - action: 'closeMenu', + onClose: { + action: 'onClose', }, }, }; diff --git a/ui/components/multichain/network-list-menu/network-list-menu.test.js b/ui/components/multichain/network-list-menu/network-list-menu.test.js index e87876f39940..cd6dae498279 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.test.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.test.js @@ -11,9 +11,11 @@ import { NetworkListMenu } from '.'; const mockSetShowTestNetworks = jest.fn(); const mockSetProviderType = jest.fn(); +const mockToggleNetworkMenu = jest.fn(); jest.mock('../../../store/actions.ts', () => ({ setShowTestNetworks: () => mockSetShowTestNetworks, setProviderType: () => mockSetProviderType, + toggleNetworkMenu: () => mockToggleNetworkMenu, })); const render = (showTestNetworks = false) => { @@ -25,7 +27,7 @@ const render = (showTestNetworks = false) => { }, }, }); - return renderWithProvider(, store); + return renderWithProvider(, store); }; describe('NetworkListMenu', () => { @@ -56,6 +58,7 @@ describe('NetworkListMenu', () => { it('switches networks when an item is clicked', () => { const { getByText } = render(); fireEvent.click(getByText(MAINNET_DISPLAY_NAME)); + expect(mockToggleNetworkMenu).toHaveBeenCalled(); expect(mockSetProviderType).toHaveBeenCalled(); }); }); diff --git a/ui/components/ui/form-field/form-field.js b/ui/components/ui/form-field/form-field.js index c9173d87db8d..283309f14d7f 100644 --- a/ui/components/ui/form-field/form-field.js +++ b/ui/components/ui/form-field/form-field.js @@ -43,6 +43,7 @@ export default function FormField({ id, inputProps, wrappingLabelProps, + inputRef, }) { return (
) : ( )} @@ -285,4 +288,8 @@ FormField.propTypes = { * If used ensure the id prop is set on the input and a label element is present using htmlFor with the same id to ensure accessibility. */ wrappingLabelProps: PropTypes.object, + /** + * ref for input component + */ + inputRef: PropTypes.object, }; diff --git a/ui/components/ui/menu/menu-item.js b/ui/components/ui/menu/menu-item.js index 3c8040f9c2c2..6e005e1eac87 100644 --- a/ui/components/ui/menu/menu-item.js +++ b/ui/components/ui/menu/menu-item.js @@ -2,8 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Text } from '../../component-library'; -import { Icon, ICON_SIZES } from '../../component-library/icon/deprecated'; +import { Text, Icon, IconSize } from '../../component-library'; import { TextVariant } from '../../../helpers/constants/design-system'; const MenuItem = ({ @@ -20,7 +19,7 @@ const MenuItem = ({ onClick={onClick} > {iconName ? ( - + ) : null}
{children}
diff --git a/ui/components/ui/numeric-input/numeric-input.component.js b/ui/components/ui/numeric-input/numeric-input.component.js index e7ac094c9091..8d16351a8daa 100644 --- a/ui/components/ui/numeric-input/numeric-input.component.js +++ b/ui/components/ui/numeric-input/numeric-input.component.js @@ -20,6 +20,7 @@ export default function NumericInput({ placeholder, id, name, + inputRef, }) { return (
{detailText && ( + {showArrow ?
: null} + {showHeader &&
} + {children ? ( + + {children} + + ) : null} {showScrollDown ? ( ) : null} - {showArrow ?
: null} - {showHeader &&
} - {children ? ( - - {children} - - ) : null} {footer ? ( ; // openMetamaskTabsIDs[tab.id]): true/false currentWindowTab: Record; // tabs.tab https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab showWhatsNewPopup: boolean; + showTermsOfUsePopup: boolean; singleExceptions: { testKey: string | null; }; @@ -109,6 +110,7 @@ const initialState: AppState = { openMetaMaskTabs: {}, currentWindowTab: {}, showWhatsNewPopup: true, + showTermsOfUsePopup: true, singleExceptions: { testKey: null, }, diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index cd3adaca8db0..7ffa78625cf1 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -26,6 +26,7 @@ const initialState = { isInitialized: false, isUnlocked: false, isAccountMenuOpen: false, + isNetworkMenuOpen: false, identities: {}, unapprovedTxs: {}, networkConfigurations: {}, @@ -102,6 +103,12 @@ export default function reduceMetamask(state = initialState, action) { isAccountMenuOpen: !metamaskState.isAccountMenuOpen, }; + case actionConstants.TOGGLE_NETWORK_MENU: + return { + ...metamaskState, + isNetworkMenuOpen: !metamaskState.isNetworkMenuOpen, + }; + case actionConstants.UPDATE_TRANSACTION_PARAMS: { const { id: txId, value } = action; let { currentNetworkTxList } = metamaskState; diff --git a/ui/ducks/metamask/metamask.test.js b/ui/ducks/metamask/metamask.test.js index 2ca94f71bdd6..92241d589326 100644 --- a/ui/ducks/metamask/metamask.test.js +++ b/ui/ducks/metamask/metamask.test.js @@ -157,6 +157,17 @@ describe('MetaMask Reducers', () => { expect(state.isAccountMenuOpen).toStrictEqual(true); }); + it('toggles network menu', () => { + const state = reduceMetamask( + {}, + { + type: actionConstants.TOGGLE_NETWORK_MENU, + }, + ); + + expect(state.isNetworkMenuOpen).toStrictEqual(true); + }); + it('updates value of tx by id', () => { const oldState = { currentNetworkTxList: [ diff --git a/ui/helpers/utils/permission.js b/ui/helpers/utils/permission.js index b931f06569cf..419c34d99811 100644 --- a/ui/helpers/utils/permission.js +++ b/ui/helpers/utils/permission.js @@ -15,13 +15,23 @@ import { } from '../../../shared/constants/permissions'; import Tooltip from '../../components/ui/tooltip'; import { + AvatarIcon, ///: BEGIN:ONLY_INCLUDE_IN(flask) Text, + Icon, ///: END:ONLY_INCLUDE_IN } from '../../components/component-library'; -import { ICON_NAMES } from '../../components/component-library/icon/deprecated'; +import { + ICON_NAMES, + ICON_SIZES, +} from '../../components/component-library/icon/deprecated'; ///: BEGIN:ONLY_INCLUDE_IN(flask) -import { Color, FONT_WEIGHT, TextVariant } from '../constants/design-system'; +import { + Color, + FONT_WEIGHT, + IconColor, + TextVariant, +} from '../constants/design-system'; import { coinTypeToProtocolName, getSnapDerivationPathName, @@ -31,19 +41,36 @@ import { const UNKNOWN_PERMISSION = Symbol('unknown'); +///: BEGIN:ONLY_INCLUDE_IN(flask) +const RIGHT_INFO_ICON = ( + +); +///: END:ONLY_INCLUDE_IN + +function getLeftIcon(iconName) { + return ( + + ); +} + export const PERMISSION_DESCRIPTIONS = deepFreeze({ [RestrictedMethods.eth_accounts]: ({ t }) => ({ label: t('permission_ethereumAccounts'), - leftIcon: ICON_NAMES.EYE, + leftIcon: getLeftIcon(ICON_NAMES.EYE), + rightIcon: null, weight: 2, }), ///: BEGIN:ONLY_INCLUDE_IN(flask) - [RestrictedMethods.snap_confirm]: ({ t }) => ({ - label: t('permission_customConfirmation'), - description: t('permission_customConfirmationDescription'), - leftIcon: ICON_NAMES.SECURITY_TICK, - weight: 3, - }), [RestrictedMethods.snap_dialog]: ({ t }) => ({ label: t('permission_dialog'), description: t('permission_dialogDescription'), @@ -251,7 +278,8 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({ [RestrictedMethods.wallet_snap]: ({ t, permissionValue }) => { const snaps = permissionValue.caveats[0].value; const baseDescription = { - leftIcon: ICON_NAMES.FLASH, + leftIcon: getLeftIcon(ICON_NAMES.FLASH), + rightIcon: RIGHT_INFO_ICON, }; return Object.keys(snaps).map((snapId) => { @@ -373,7 +401,8 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({ ///: END:ONLY_INCLUDE_IN [UNKNOWN_PERMISSION]: ({ t, permissionName }) => ({ label: t('permission_unknown', [permissionName ?? 'undefined']), - leftIcon: ICON_NAMES.QUESTION, + leftIcon: getLeftIcon(ICON_NAMES.QUESTION), + rightIcon: null, weight: 4, }), }); diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index 4d1f8dfbf9ca..e918c494f484 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -11,6 +11,8 @@ import Button from '../../../components/ui/button'; import SimulationErrorMessage from '../../../components/ui/simulation-error-message'; import EditGasFeeButton from '../../../components/app/edit-gas-fee-button'; import MultiLayerFeeMessage from '../../../components/app/multilayer-fee-message'; +import SecurityProviderBannerMessage from '../../../components/app/security-provider-banner-message/security-provider-banner-message'; +import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../../../components/app/security-provider-banner-message/security-provider-banner-message.constants'; import { BLOCK_SIZES, JustifyContent, @@ -556,6 +558,15 @@ export default class ConfirmApproveContent extends Component { 'confirm-approve-content--full': showFullTxDetails, })} > + {(txData?.securityProviderResponse?.flagAsDangerous !== undefined && + txData?.securityProviderResponse?.flagAsDangerous !== + SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) || + (txData?.securityProviderResponse && + Object.keys(txData.securityProviderResponse).length === 0) ? ( + + ) : null} {warning && (
diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js index 1b89d136db1c..c82da63b80a9 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js +++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.test.js @@ -325,4 +325,22 @@ describe('ConfirmApproveContent Component', () => { expect(container).toMatchSnapshot(); }); + + it('should render security provider response if transaction is malicious', () => { + const securityProviderResponse = { + flagAsDangerous: 1, + reason: + 'This has been flagged as potentially suspicious. If you sign, you could lose access to all of your NFTs and any funds or other assets in your wallet.', + reason_header: 'Warning', + }; + const { getByText } = renderComponent({ + ...props, + txData: { + ...props.txData, + securityProviderResponse, + }, + }); + + expect(getByText(securityProviderResponse.reason)).toBeInTheDocument(); + }); }); diff --git a/ui/pages/confirmation/templates/flask/snap-confirmation/snap-confirmation.js b/ui/pages/confirmation/templates/flask/snap-confirmation/snap-confirmation.js index 942ebab9cf6a..6b7d71b2a7f9 100644 --- a/ui/pages/confirmation/templates/flask/snap-confirmation/snap-confirmation.js +++ b/ui/pages/confirmation/templates/flask/snap-confirmation/snap-confirmation.js @@ -1,11 +1,10 @@ -import { TypographyVariant } from '../../../../../helpers/constants/design-system'; import { mapToTemplate } from '../../../../../components/app/flask/snap-ui-renderer'; import { DelineatorType } from '../../../../../helpers/constants/flask'; function getValues(pendingApproval, t, actions) { const { snapName, - requestData: { content, title, description, textAreaContent }, + requestData: { content }, } = pendingApproval; return { @@ -25,49 +24,7 @@ function getValues(pendingApproval, t, actions) { snapName, }, // TODO: Replace with SnapUIRenderer when we don't need to inject the input manually. - // TODO: Remove ternary once snap_confirm has been removed. - children: content - ? mapToTemplate(content) - : [ - { - element: 'Typography', - key: 'title', - children: title, - props: { - variant: TypographyVariant.H3, - fontWeight: 'bold', - boxProps: { - marginBottom: 4, - }, - }, - }, - ...(description - ? [ - { - element: 'Typography', - key: 'subtitle', - children: description, - props: { - variant: TypographyVariant.H6, - boxProps: { - marginBottom: 4, - }, - }, - }, - ] - : []), - ...(textAreaContent - ? [ - { - element: 'Copyable', - key: 'snap-dialog-content-text', - props: { - text: textAreaContent, - }, - }, - ] - : []), - ], + children: mapToTemplate(content), }, }, ], diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index c525bd161e27..616256f215a1 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -2,12 +2,14 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { Redirect, Route } from 'react-router-dom'; ///: BEGIN:ONLY_INCLUDE_IN(main) +// eslint-disable-next-line import/no-duplicates +import { MetaMetricsContextProp } from '../../../shared/constants/metametrics'; +///: END:ONLY_INCLUDE_IN import { - MetaMetricsContextProp, MetaMetricsEventCategory, MetaMetricsEventName, + // eslint-disable-next-line import/no-duplicates } from '../../../shared/constants/metametrics'; -///: END:ONLY_INCLUDE_IN import AssetList from '../../components/app/asset-list'; import NftsTab from '../../components/app/nfts-tab'; import HomeNotification from '../../components/app/home-notification'; @@ -22,6 +24,7 @@ import ConnectedAccounts from '../connected-accounts'; import { Tabs, Tab } from '../../components/ui/tabs'; import { EthOverview } from '../../components/app/wallet-overview'; import WhatsNewPopup from '../../components/app/whats-new-popup'; +import TermsOfUsePopup from '../../components/app/terms-of-use-popup'; import RecoveryPhraseReminder from '../../components/app/recovery-phrase-reminder'; import ActionableMessage from '../../components/ui/actionable-message/actionable-message'; import { @@ -111,6 +114,7 @@ export default class Home extends PureComponent { infuraBlocked: PropTypes.bool.isRequired, showWhatsNewPopup: PropTypes.bool.isRequired, hideWhatsNewPopup: PropTypes.func.isRequired, + showTermsOfUsePopup: PropTypes.bool.isRequired, announcementsToShow: PropTypes.bool.isRequired, ///: BEGIN:ONLY_INCLUDE_IN(flask) errorsToShow: PropTypes.object.isRequired, @@ -120,6 +124,7 @@ export default class Home extends PureComponent { showRecoveryPhraseReminder: PropTypes.bool.isRequired, setRecoveryPhraseReminderHasBeenShown: PropTypes.func.isRequired, setRecoveryPhraseReminderLastShown: PropTypes.func.isRequired, + setTermsOfUseLastAgreed: PropTypes.func.isRequired, showOutdatedBrowserWarning: PropTypes.bool.isRequired, setOutdatedBrowserWarningLastShown: PropTypes.func.isRequired, seedPhraseBackedUp: (props) => { @@ -243,6 +248,18 @@ export default class Home extends PureComponent { setRecoveryPhraseReminderLastShown(new Date().getTime()); }; + onAcceptTermsOfUse = () => { + const { setTermsOfUseLastAgreed } = this.props; + setTermsOfUseLastAgreed(new Date().getTime()); + this.context.trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsEventName.TermsOfUseAccepted, + properties: { + location: 'Terms Of Use Popover', + }, + }); + }; + onOutdatedBrowserWarningClose = () => { const { setOutdatedBrowserWarningLastShown } = this.props; setOutdatedBrowserWarningLastShown(new Date().getTime()); @@ -600,6 +617,7 @@ export default class Home extends PureComponent { announcementsToShow, showWhatsNewPopup, hideWhatsNewPopup, + showTermsOfUsePopup, seedPhraseBackedUp, showRecoveryPhraseReminder, firstTimeFlowType, @@ -621,6 +639,10 @@ export default class Home extends PureComponent { showWhatsNewPopup && !process.env.IN_TEST && !newNetworkAddedConfigurationId; + + const showTermsOfUse = + completedOnboarding && !onboardedInThisUISession && showTermsOfUsePopup; + return (
@@ -637,11 +659,14 @@ export default class Home extends PureComponent { onConfirm={this.onRecoveryPhraseReminderClose} /> ) : null} + {showTermsOfUse ? ( + + ) : null} {isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() : null}
- + {process.env.MULTICHAIN ? null : }
diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index be0ef90c3a1c..0601c7e1ec9a 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -17,6 +17,7 @@ import { getShowWhatsNewPopup, getSortedAnnouncementsToShow, getShowRecoveryPhraseReminder, + getShowTermsOfUse, getShowOutdatedBrowserWarning, getNewNetworkAdded, hasUnsignedQRHardwareTransaction, @@ -35,6 +36,7 @@ import { setAlertEnabledness, setRecoveryPhraseReminderHasBeenShown, setRecoveryPhraseReminderLastShown, + setTermsOfUseLastAgreed, setOutdatedBrowserWarningLastShown, setNewNetworkAdded, setNewNftAddedMessage, @@ -136,6 +138,7 @@ const mapStateToProps = (state) => { ///: END:ONLY_INCLUDE_IN showWhatsNewPopup: getShowWhatsNewPopup(state), showRecoveryPhraseReminder: getShowRecoveryPhraseReminder(state), + showTermsOfUsePopup: getShowTermsOfUse(state), showOutdatedBrowserWarning: getIsBrowserDeprecated() && getShowOutdatedBrowserWarning(state), seedPhraseBackedUp, @@ -166,6 +169,9 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(setRecoveryPhraseReminderHasBeenShown()), setRecoveryPhraseReminderLastShown: (lastShown) => dispatch(setRecoveryPhraseReminderLastShown(lastShown)), + setTermsOfUseLastAgreed: (lastAgreed) => { + dispatch(setTermsOfUseLastAgreed(lastAgreed)); + }, setOutdatedBrowserWarningLastShown: (lastShown) => { dispatch(setOutdatedBrowserWarningLastShown(lastShown)); }, diff --git a/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap b/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap index 2d68b55319b0..10ee36b9b161 100644 --- a/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap +++ b/ui/pages/institutional/confirm-add-institutional-feature/__snapshots__/confirm-add-institutional-feature.test.js.snap @@ -42,26 +42,28 @@ exports[`Confirm Add Institutional Feature opens confirm institutional sucessful

- +
+
`; diff --git a/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js index 25a52fc16188..0e89910e286e 100644 --- a/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js +++ b/ui/pages/institutional/confirm-add-institutional-feature/confirm-add-institutional-feature.js @@ -7,12 +7,17 @@ import PulseLoader from '../../../components/ui/pulse-loader'; import { INSTITUTIONAL_FEATURES_DONE_ROUTE } from '../../../helpers/constants/routes'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; -import { Text } from '../../../components/component-library'; +import { + Text, + BUTTON_SIZES, + BUTTON_TYPES, +} from '../../../components/component-library'; import { TextColor, TextVariant, OVERFLOW_WRAP, TEXT_ALIGN, + DISPLAY, } from '../../../helpers/constants/design-system'; import Box from '../../../components/ui/box'; import { mmiActionsFactory } from '../../../store/institutional/institution-background'; @@ -169,16 +174,15 @@ export default function ConfirmAddInstitutionalFeature({ history }) { )} - + {isLoading ? ( -
- -
+ ) : ( -
+ -
+
)}
diff --git a/ui/pages/onboarding-flow/welcome/index.scss b/ui/pages/onboarding-flow/welcome/index.scss index a2cab4582e25..595adf152424 100644 --- a/ui/pages/onboarding-flow/welcome/index.scss +++ b/ui/pages/onboarding-flow/welcome/index.scss @@ -51,4 +51,9 @@ margin-bottom: 24px; } } + + &__terms-checkbox { + margin: 0; + align-self: flex-start; + } } diff --git a/ui/pages/onboarding-flow/welcome/welcome.js b/ui/pages/onboarding-flow/welcome/welcome.js index 042282dc285f..00f83d79ae86 100644 --- a/ui/pages/onboarding-flow/welcome/welcome.js +++ b/ui/pages/onboarding-flow/welcome/welcome.js @@ -6,10 +6,13 @@ import { Carousel } from 'react-responsive-carousel'; import Mascot from '../../../components/ui/mascot'; import Button from '../../../components/ui/button'; import { Text } from '../../../components/component-library'; +import CheckBox from '../../../components/ui/check-box'; +import Box from '../../../components/ui/box'; import { FONT_WEIGHT, TEXT_ALIGN, TextVariant, + AlignItems, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -17,7 +20,10 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { setFirstTimeFlowType } from '../../../store/actions'; +import { + setFirstTimeFlowType, + setTermsOfUseLastAgreed, +} from '../../../store/actions'; import { ONBOARDING_METAMETRICS, ONBOARDING_SECURE_YOUR_WALLET_ROUTE, @@ -33,6 +39,7 @@ export default function OnboardingWelcome() { const [eventEmitter] = useState(new EventEmitter()); const currentKeyring = useSelector(getCurrentKeyring); const firstTimeFlowType = useSelector(getFirstTimeFlowType); + const [termsChecked, setTermsChecked] = useState(false); // Don't allow users to come back to this screen after they // have already imported or created a wallet @@ -56,8 +63,23 @@ export default function OnboardingWelcome() { account_type: 'metamask', }, }); + dispatch(setTermsOfUseLastAgreed(new Date().getTime())); history.push(ONBOARDING_METAMETRICS); }; + const toggleTermsCheck = () => { + setTermsChecked((currentTermsChecked) => !currentTermsChecked); + }; + const termsOfUse = t('agreeTermsOfUse', [ + + {t('terms')} + , + ]); const onImportClick = () => { dispatch(setFirstTimeFlowType('import')); @@ -68,6 +90,7 @@ export default function OnboardingWelcome() { account_type: 'imported', }, }); + dispatch(setTermsOfUseLastAgreed(new Date().getTime())); history.push(ONBOARDING_METAMETRICS); }; @@ -147,11 +170,35 @@ export default function OnboardingWelcome() {
    +
  • + + + + +
  • +
  • @@ -161,6 +208,7 @@ export default function OnboardingWelcome() { data-testid="onboarding-import-wallet" type="secondary" onClick={onImportClick} + disabled={!termsChecked} > {t('onboardingImportWallet')} diff --git a/ui/pages/onboarding-flow/welcome/welcome.test.js b/ui/pages/onboarding-flow/welcome/welcome.test.js index 8cd699012ff7..c7965efd52f4 100644 --- a/ui/pages/onboarding-flow/welcome/welcome.test.js +++ b/ui/pages/onboarding-flow/welcome/welcome.test.js @@ -4,7 +4,10 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import initializedMockState from '../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; -import { setFirstTimeFlowType } from '../../../store/actions'; +import { + setFirstTimeFlowType, + setTermsOfUseLastAgreed, +} from '../../../store/actions'; import { ONBOARDING_METAMETRICS, ONBOARDING_SECURE_YOUR_WALLET_ROUTE, @@ -21,6 +24,11 @@ jest.mock('../../../store/actions.ts', () => ({ return type; }), ), + setTermsOfUseLastAgreed: jest.fn().mockReturnValue( + jest.fn((type) => { + return type; + }), + ), })); jest.mock('react-router-dom', () => ({ @@ -81,19 +89,24 @@ describe('Onboarding Welcome Component', () => { it('should set first time flow to create and route to metametrics', () => { renderWithProvider(, mockStore); - + const termsCheckbox = screen.getByTestId('onboarding-terms-checkbox'); + fireEvent.click(termsCheckbox); const createWallet = screen.getByTestId('onboarding-create-wallet'); fireEvent.click(createWallet); + expect(setTermsOfUseLastAgreed).toHaveBeenCalled(); expect(setFirstTimeFlowType).toHaveBeenCalledWith('create'); }); it('should set first time flow to import and route to metametrics', () => { renderWithProvider(, mockStore); + const termsCheckbox = screen.getByTestId('onboarding-terms-checkbox'); + fireEvent.click(termsCheckbox); const createWallet = screen.getByTestId('onboarding-import-wallet'); fireEvent.click(createWallet); + expect(setTermsOfUseLastAgreed).toHaveBeenCalled(); expect(setFirstTimeFlowType).toHaveBeenCalledWith('import'); expect(mockHistoryPush).toHaveBeenCalledWith(ONBOARDING_METAMETRICS); }); diff --git a/ui/pages/permissions-connect/flask/snap-install/snap-install.js b/ui/pages/permissions-connect/flask/snap-install/snap-install.js index 34a0cbcd3e7a..d4b7126239ec 100644 --- a/ui/pages/permissions-connect/flask/snap-install/snap-install.js +++ b/ui/pages/permissions-connect/flask/snap-install/snap-install.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import { PageContainerFooter } from '../../../../components/ui/page-container'; -import PermissionsConnectPermissionList from '../../../../components/app/permissions-connect-permission-list'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import SnapInstallWarning from '../../../../components/app/flask/snap-install-warning'; import Box from '../../../../components/ui/box/box'; @@ -21,6 +20,7 @@ import SnapAuthorship from '../../../../components/app/flask/snap-authorship'; import { Text } from '../../../../components/component-library'; import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; import { getSnapName } from '../../../../helpers/utils/util'; +import SnapPermissionsList from '../../../../components/app/flask/snap-permissions-list'; export default function SnapInstall({ request, @@ -87,12 +87,16 @@ export default function SnapInstall({ className="headers" alignItems={AlignItems.center} flexDirection={FLEX_DIRECTION.COLUMN} - paddingLeft={4} - paddingRight={4} > - + + + {!hasError && ( - + {t('snapInstall')} )} @@ -114,6 +118,8 @@ export default function SnapInstall({ {t('snapInstallRequestsPermission', [ @@ -121,7 +127,7 @@ export default function SnapInstall({ {snapName}, ])} - diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 8cb160764239..fae50114228f 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -31,6 +31,11 @@ import AccountMenu from '../../components/app/account-menu'; import { Modal } from '../../components/app/modals'; import Alert from '../../components/ui/alert'; import AppHeader from '../../components/app/app-header'; +import { + AppHeader as MultichainAppHeader, + AccountListMenu, + NetworkListMenu, +} from '../../components/multichain'; import UnlockPage from '../unlock-page'; import Alerts from '../../components/app/alerts'; import Asset from '../asset'; @@ -90,7 +95,6 @@ import { SEND_STAGES } from '../../ducks/send'; import DeprecatedTestNetworks from '../../components/ui/deprecated-test-networks/deprecated-test-networks'; import NewNetworkInfo from '../../components/ui/new-network-info/new-network-info'; import { ThemeType } from '../../../shared/constants/preferences'; -import { AccountListMenu } from '../../components/multichain'; export default class Routes extends Component { static propTypes = { @@ -128,6 +132,8 @@ export default class Routes extends Component { completedOnboarding: PropTypes.bool, isAccountMenuOpen: PropTypes.bool, toggleAccountMenu: PropTypes.func, + isNetworkMenuOpen: PropTypes.bool, + toggleNetworkMenu: PropTypes.func, }; static contextTypes = { @@ -322,7 +328,11 @@ export default class Routes extends Component { } onEditTransactionPage() { - return this.props.sendStage === SEND_STAGES.EDIT; + return ( + this.props.sendStage === SEND_STAGES.EDIT || + this.props.sendStage === SEND_STAGES.DRAFT || + this.props.sendStage === SEND_STAGES.ADD_RECIPIENT + ); } onSwapsPage() { @@ -432,6 +442,8 @@ export default class Routes extends Component { completedOnboarding, isAccountMenuOpen, toggleAccountMenu, + isNetworkMenuOpen, + toggleNetworkMenu, } = this.props; const loadMessage = loadingMessage || isNetworkLoading @@ -474,24 +486,30 @@ export default class Routes extends Component { - {!this.hideAppHeader() && ( - - )} + {!this.hideAppHeader() && + (process.env.MULTICHAIN ? ( + + ) : ( + + ))} {this.showOnboardingHeader() && } {completedOnboarding ? : null} {process.env.MULTICHAIN ? null : } {process.env.MULTICHAIN && isAccountMenuOpen ? ( toggleAccountMenu()} /> ) : null} + {process.env.MULTICHAIN && isNetworkMenuOpen ? ( + toggleNetworkMenu()} /> + ) : null}
    {isLoading ? : null} {!isLoading && isNetworkLoading ? : null} diff --git a/ui/pages/routes/routes.component.test.js b/ui/pages/routes/routes.component.test.js new file mode 100644 index 000000000000..fba5da41fe25 --- /dev/null +++ b/ui/pages/routes/routes.component.test.js @@ -0,0 +1,122 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { fireEvent } from '@testing-library/react'; + +import { SEND_STAGES } from '../../ducks/send'; +import { renderWithProvider } from '../../../test/jest'; +import mockSendState from '../../../test/data/mock-send-state.json'; +import Routes from '.'; + +const mockShowNetworkDropdown = jest.fn(); +const mockHideNetworkDropdown = jest.fn(); + +jest.mock('webextension-polyfill', () => ({ + runtime: { + onMessage: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, + getManifest: () => ({ manifest_version: 2 }), + }, +})); + +jest.mock('../../store/actions', () => ({ + getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()), + getGasFeeEstimatesAndStartPolling: jest + .fn() + .mockImplementation(() => Promise.resolve()), + addPollingTokenToAppState: jest.fn(), + showNetworkDropdown: () => mockShowNetworkDropdown, + hideNetworkDropdown: () => mockHideNetworkDropdown, +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn(), + }), +})); + +jest.mock('../../ducks/send', () => ({ + ...jest.requireActual('../../ducks/send'), + resetSendState: () => ({ type: 'XXX' }), + getGasPrice: jest.fn(), +})); + +jest.mock('../../ducks/domains', () => ({ + ...jest.requireActual('../../ducks/domains'), + initializeDomainSlice: () => ({ type: 'XXX' }), +})); + +describe('Routes Component', () => { + afterEach(() => { + mockShowNetworkDropdown.mockClear(); + mockHideNetworkDropdown.mockClear(); + }); + describe('render during send flow', () => { + it('should render with network and account change disabled while adding recipient for send flow', () => { + const store = configureMockStore()({ + ...mockSendState, + send: { + ...mockSendState.send, + stage: SEND_STAGES.ADD_RECIPIENT, + }, + }); + const { getByTestId } = renderWithProvider(, store, ['/send']); + + expect(getByTestId('account-menu-icon')).toBeDisabled(); + + const networkDisplay = getByTestId('network-display'); + fireEvent.click(networkDisplay); + expect(mockShowNetworkDropdown).not.toHaveBeenCalled(); + }); + it('should render with network and account change disabled while user is in send page', () => { + const store = configureMockStore()({ + ...mockSendState, + }); + const { getByTestId } = renderWithProvider(, store, ['/send']); + + expect(getByTestId('account-menu-icon')).toBeDisabled(); + + const networkDisplay = getByTestId('network-display'); + fireEvent.click(networkDisplay); + expect(mockShowNetworkDropdown).not.toHaveBeenCalled(); + }); + it('should render with network and account change disabled while editing a send transaction', () => { + const store = configureMockStore()({ + ...mockSendState, + send: { + ...mockSendState.send, + stage: SEND_STAGES.EDIT, + }, + }); + const { getByTestId } = renderWithProvider(, store, ['/send']); + + expect(getByTestId('account-menu-icon')).toBeDisabled(); + + const networkDisplay = getByTestId('network-display'); + fireEvent.click(networkDisplay); + expect(mockShowNetworkDropdown).not.toHaveBeenCalled(); + }); + it('should render when send transaction is not active', () => { + const store = configureMockStore()({ + ...mockSendState, + metamask: { + ...mockSendState.metamask, + swapsState: { + ...mockSendState.metamask.swapsState, + swapsFeatureIsLive: true, + }, + pendingApprovals: {}, + announcements: {}, + }, + send: { + ...mockSendState.send, + stage: SEND_STAGES.INACTIVE, + }, + }); + const { getByTestId } = renderWithProvider(, store); + expect(getByTestId('account-menu-icon')).not.toBeDisabled(); + }); + }); +}); diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index c132bcfa87c7..3f5c857ba3dd 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -19,6 +19,7 @@ import { setLastActiveTime, setMouseUserState, toggleAccountMenu, + toggleNetworkMenu, } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; @@ -57,6 +58,7 @@ function mapStateToProps(state) { isCurrentProviderCustom: isCurrentProviderCustom(state), completedOnboarding, isAccountMenuOpen: state.metamask.isAccountMenuOpen, + isNetworkMenuOpen: state.metamask.isNetworkMenuOpen, }; } @@ -70,6 +72,7 @@ function mapDispatchToProps(dispatch) { pageChanged: (path) => dispatch(pageChanged(path)), prepareToLeaveSwaps: () => dispatch(prepareToLeaveSwaps()), toggleAccountMenu: () => dispatch(toggleAccountMenu()), + toggleNetworkMenu: () => dispatch(toggleNetworkMenu()), }; } diff --git a/ui/pages/settings/flask/view-snap/view-snap.js b/ui/pages/settings/flask/view-snap/view-snap.js index 3261652d9886..a9ab3a0965ad 100644 --- a/ui/pages/settings/flask/view-snap/view-snap.js +++ b/ui/pages/settings/flask/view-snap/view-snap.js @@ -19,7 +19,6 @@ import SnapAuthorship from '../../../../components/app/flask/snap-authorship'; import Box from '../../../../components/ui/box'; import SnapRemoveWarning from '../../../../components/app/flask/snap-remove-warning'; import ToggleButton from '../../../../components/ui/toggle-button'; -import PermissionsConnectPermissionList from '../../../../components/app/permissions-connect-permission-list/permissions-connect-permission-list'; import ConnectedSitesList from '../../../../components/app/connected-sites-list'; import Tooltip from '../../../../components/ui/tooltip'; import { SNAPS_LIST_ROUTE } from '../../../../helpers/constants/routes'; @@ -38,6 +37,7 @@ import { getTargetSubjectMetadata, } from '../../../../selectors'; import { formatDate } from '../../../../helpers/utils/util'; +import SnapPermissionsList from '../../../../components/app/flask/snap-permissions-list'; function ViewSnap() { const t = useI18nContext(); @@ -182,7 +182,7 @@ function ViewSnap() { {t('snapAccess', [snap.manifest.proposedName])} - diff --git a/ui/pages/token-allowance/token-allowance.js b/ui/pages/token-allowance/token-allowance.js index ef1583ef05d9..7ee7fd887f33 100644 --- a/ui/pages/token-allowance/token-allowance.js +++ b/ui/pages/token-allowance/token-allowance.js @@ -67,6 +67,8 @@ import { ICON_NAMES, } from '../../components/component-library/icon/deprecated'; import LedgerInstructionField from '../../components/app/ledger-instruction-field/ledger-instruction-field'; +import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../../components/app/security-provider-banner-message/security-provider-banner-message.constants'; +import SecurityProviderBannerMessage from '../../components/app/security-provider-banner-message/security-provider-banner-message'; const ALLOWED_HOSTS = ['portfolio.metamask.io']; @@ -272,6 +274,15 @@ export default function TokenAllowance({ + {(txData?.securityProviderResponse?.flagAsDangerous !== undefined && + txData?.securityProviderResponse?.flagAsDangerous !== + SECURITY_PROVIDER_MESSAGE_SEVERITIES.NOT_MALICIOUS) || + (txData?.securityProviderResponse && + Object.keys(txData.securityProviderResponse).length === 0) ? ( + + ) : null} { expect(queryByText('Prior to clicking confirm:')).toBeNull(); }); + + it('should render security provider response if transaction is malicious', () => { + const securityProviderResponse = { + flagAsDangerous: 1, + reason: + 'This has been flagged as potentially suspicious. If you sign, you could lose access to all of your NFTs and any funds or other assets in your wallet.', + reason_header: 'Warning', + }; + const { getByText } = renderWithProvider( + , + store, + ); + + expect(getByText(securityProviderResponse.reason)).toBeInTheDocument(); + }); }); diff --git a/ui/selectors/institutional/selectors.js b/ui/selectors/institutional/selectors.js new file mode 100644 index 000000000000..6fe4a7918d75 --- /dev/null +++ b/ui/selectors/institutional/selectors.js @@ -0,0 +1,64 @@ +import { toChecksumAddress } from 'ethereumjs-util'; +import { getSelectedIdentity, getAccountType, getProvider } from '../selectors'; + +export function getWaitForConfirmDeepLinkDialog(state) { + return state.metamask.waitForConfirmDeepLinkDialog; +} + +export function getTransactionStatusMap(state) { + return state.metamask.custodyStatusMaps; +} + +export function getCustodyAccountDetails(state) { + return state.metamask.custodyAccountDetails; +} + +export function getCustodyAccountSupportedChains(state, address) { + return state.metamask.custodianSupportedChains + ? state.metamask.custodianSupportedChains[toChecksumAddress(address)] + : []; +} + +export function getMmiPortfolioEnabled(state) { + return state.metamask.mmiConfiguration?.portfolio?.enabled; +} + +export function getMmiPortfolioUrl(state) { + return state.metamask.mmiConfiguration?.portfolio?.url; +} + +export function getConfiguredCustodians(state) { + return state.metamask.mmiConfiguration?.custodians || []; +} + +export function getCustodianIconForAddress(state, address) { + let custodianIcon; + + const checksummedAddress = toChecksumAddress(address); + if (state.metamask.custodyAccountDetails?.[checksummedAddress]) { + const { custodianName } = + state.metamask.custodyAccountDetails[checksummedAddress]; + custodianIcon = state.metamask.mmiConfiguration?.custodians?.find( + (custodian) => custodian.name === custodianName, + )?.iconUrl; + } + + return custodianIcon; +} + +export function getIsCustodianSupportedChain(state) { + const selectedIdentity = getSelectedIdentity(state); + const accountType = getAccountType(state); + const provider = getProvider(state); + + const supportedChains = + accountType === 'custody' + ? getCustodyAccountSupportedChains(state, selectedIdentity.address) + : null; + + return supportedChains?.supportedChains + ? supportedChains.supportedChains.includes( + Number(provider.chainId).toString(), + ) + : true; +} diff --git a/ui/selectors/institutional/selectors.test.js b/ui/selectors/institutional/selectors.test.js new file mode 100644 index 000000000000..1cbf02213b95 --- /dev/null +++ b/ui/selectors/institutional/selectors.test.js @@ -0,0 +1,151 @@ +import { toChecksumAddress } from 'ethereumjs-util'; +import { + getConfiguredCustodians, + getCustodianIconForAddress, + getCustodyAccountDetails, + getCustodyAccountSupportedChains, + getMmiPortfolioEnabled, + getMmiPortfolioUrl, + getTransactionStatusMap, + getWaitForConfirmDeepLinkDialog, + getIsCustodianSupportedChain, +} from './selectors'; + +describe('Institutional selectors', () => { + const state = { + metamask: { + provider: { + type: 'test', + chainId: '1', + }, + identities: { + '0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275': { + name: 'Custody Account A', + address: '0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275', + }, + }, + selectedAddress: '0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275', + waitForConfirmDeepLinkDialog: '123', + keyrings: [ + { + type: 'Custody', + accounts: ['0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275'], + }, + ], + custodyStatusMaps: '123', + custodyAccountDetails: { + '0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275': { + custodianName: 'saturn', + }, + }, + custodianSupportedChains: { + '0x5Ab19e7091dD208F352F8E727B6DCC6F8aBB6275': { + supportedChains: ['1', '2'], + custodianName: 'saturn', + }, + }, + mmiConfiguration: { + portfolio: { + enabled: true, + url: 'https://dashboard.metamask-institutional.io', + }, + custodians: [ + { + type: 'saturn', + name: 'saturn', + apiUrl: 'https://saturn-custody.dev.metamask-institutional.io', + iconUrl: 'images/saturn.svg', + displayName: 'Saturn Custody', + production: true, + refreshTokenUrl: null, + isNoteToTraderSupported: false, + version: 1, + }, + ], + }, + }, + }; + + describe('getWaitForConfirmDeepLinkDialog', () => { + it('extracts a state property', () => { + const result = getWaitForConfirmDeepLinkDialog(state); + expect(result).toStrictEqual(state.metamask.waitForConfirmDeepLinkDialog); + }); + }); + + describe('getCustodyAccountDetails', () => { + it('extracts a state property', () => { + const result = getCustodyAccountDetails(state); + expect(result).toStrictEqual(state.metamask.custodyAccountDetails); + }); + }); + + describe('getTransactionStatusMap', () => { + it('extracts a state property', () => { + const result = getTransactionStatusMap(state); + expect(result).toStrictEqual(state.metamask.custodyStatusMaps); + }); + }); + + describe('getCustodianSupportedChains', () => { + it('extracts a state property', () => { + const result = getCustodyAccountSupportedChains( + state, + '0x5ab19e7091dd208f352f8e727b6dcc6f8abb6275', + ); + expect(result).toStrictEqual( + state.metamask.custodianSupportedChains[ + toChecksumAddress('0x5ab19e7091dd208f352f8e727b6dcc6f8abb6275') + ], + ); + }); + }); + + describe('getMmiPortfolioEnabled', () => { + it('extracts a state property', () => { + const result = getMmiPortfolioEnabled(state); + expect(result).toStrictEqual( + state.metamask.mmiConfiguration.portfolio.enabled, + ); + }); + }); + + describe('getMmiPortfolioUrl', () => { + it('extracts a state property', () => { + const result = getMmiPortfolioUrl(state); + expect(result).toStrictEqual( + state.metamask.mmiConfiguration.portfolio.url, + ); + }); + }); + + describe('getConfiguredCustodians', () => { + it('extracts a state property', () => { + const result = getConfiguredCustodians(state); + expect(result).toStrictEqual(state.metamask.mmiConfiguration.custodians); + }); + }); + + describe('getCustodianIconForAddress', () => { + it('extracts a state property', () => { + const result = getCustodianIconForAddress( + state, + '0x5ab19e7091dd208f352f8e727b6dcc6f8abb6275', + ); + + expect(result).toStrictEqual( + state.metamask.mmiConfiguration.custodians[0].iconUrl, + ); + }); + }); + + describe('getIsCustodianSupportedChain', () => { + it('extracts a state property', () => { + const result = getIsCustodianSupportedChain( + state, + '0x5ab19e7091dd208f352f8e727b6dcc6f8abb6275', + ); + expect(result).toStrictEqual(true); + }); + }); +}); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 1b48a1e849f8..4b4c3aba89dd 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -57,6 +57,7 @@ import { import { TEMPLATED_CONFIRMATION_MESSAGE_TYPES } from '../pages/confirmation/templates'; import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens'; import { DAY } from '../../shared/constants/time'; +import { TERMS_OF_USE_LAST_UPDATED } from '../../shared/constants/terms'; import { getNativeCurrency, getConversionRate, @@ -227,6 +228,12 @@ export function getAccountType(state) { const currentKeyring = getCurrentKeyring(state); const type = currentKeyring && currentKeyring.type; + ///: BEGIN:ONLY_INCLUDE_IN(mmi) + if (type.startsWith('Custody')) { + return 'custody'; + } + ///: END:ONLY_INCLUDE_IN + switch (type) { case KeyringType.trezor: case KeyringType.ledger: @@ -995,6 +1002,18 @@ export function getShowRecoveryPhraseReminder(state) { return currentTime - recoveryPhraseReminderLastShown >= frequency; } +export function getShowTermsOfUse(state) { + const { termsOfUseLastAgreed } = state.metamask; + + if (!termsOfUseLastAgreed) { + return true; + } + return ( + new Date(termsOfUseLastAgreed).getTime() < + new Date(TERMS_OF_USE_LAST_UPDATED).getTime() + ); +} + export function getShowOutdatedBrowserWarning(state) { const { outdatedBrowserWarningLastShown } = state.metamask; if (!outdatedBrowserWarningLastShown) { @@ -1111,6 +1130,13 @@ export function getNetworkConfigurations(state) { return state.metamask.networkConfigurations; } +export function getCurrentNetwork(state) { + const allNetworks = getAllNetworks(state); + const currentChainId = getCurrentChainId(state); + + return allNetworks.find((network) => network.chainId === currentChainId); +} + export function getAllNetworks(state) { const networkConfigurations = getNetworkConfigurations(state) || {}; const showTestnetNetworks = getShowTestNetworks(state); diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index b58276ba9cb7..34e1e9dfc3f5 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -48,6 +48,7 @@ export const SHOW_LOADING = 'SHOW_LOADING_INDICATION'; export const HIDE_LOADING = 'HIDE_LOADING_INDICATION'; export const TOGGLE_ACCOUNT_MENU = 'TOGGLE_ACCOUNT_MENU'; +export const TOGGLE_NETWORK_MENU = 'TOGGLE_NETWORK_MENU'; // preferences export const UPDATE_CUSTOM_NONCE = 'UPDATE_CUSTOM_NONCE'; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 97be01ad56d0..dd897d303880 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3122,6 +3122,12 @@ export function toggleAccountMenu() { }; } +export function toggleNetworkMenu() { + return { + type: actionConstants.TOGGLE_NETWORK_MENU, + }; +} + export function setParticipateInMetaMetrics( participationPreference: boolean, ): ThunkAction< @@ -3974,6 +3980,12 @@ export function setRecoveryPhraseReminderLastShown( }; } +export function setTermsOfUseLastAgreed(lastAgreed: number) { + return async () => { + await submitRequestToBackground('setTermsOfUseLastAgreed', [lastAgreed]); + }; +} + export function setOutdatedBrowserWarningLastShown(lastShown: number) { return async () => { await submitRequestToBackground('setOutdatedBrowserWarningLastShown', [ diff --git a/yarn.lock b/yarn.lock index b2d8e4cee53e..080233a0b148 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3286,6 +3286,13 @@ __metadata: languageName: node linkType: hard +"@json-rpc-specification/meta-schema@npm:^1.0.6": + version: 1.0.6 + resolution: "@json-rpc-specification/meta-schema@npm:1.0.6" + checksum: 2eb9c6c6c73bb38350c7180d1ad3c5b8462406926cae753741895b457d7b1b9f0b74148daf3462bb167cef39efdd1d9090308edf4d4938956863acb643c146eb + languageName: node + linkType: hard + "@keystonehq/base-eth-keyring@npm:^0.7.1": version: 0.7.1 resolution: "@keystonehq/base-eth-keyring@npm:0.7.1" @@ -3595,16 +3602,16 @@ __metadata: languageName: node linkType: hard -"@metamask/approval-controller@npm:^2.0.0": - version: 2.0.0 - resolution: "@metamask/approval-controller@npm:2.0.0" +"@metamask/approval-controller@npm:^2.0.0, @metamask/approval-controller@npm:^2.1.0": + version: 2.1.0 + resolution: "@metamask/approval-controller@npm:2.1.0" dependencies: "@metamask/base-controller": ^2.0.0 - "@metamask/controller-utils": ^3.0.0 + "@metamask/controller-utils": ^3.1.0 eth-rpc-errors: ^4.0.0 immer: ^9.0.6 nanoid: ^3.1.31 - checksum: 1db5f9c21b04fa4688c17cdfb7da0a14b3fee084fbd8c0cfdcc41572e54140ce093c24b811b85e8ee9d3ccd8987db04d9150d7c6d5ab21daf72b4364a05f3428 + checksum: 207380e3ed0007aec3b9efcde62ac3ece9fa46cc7b9e6157c0d54271e0936b5a9a05e59adcfb1e47e3f3df397d2d2dc757f3b97745528695182e7c66a5207aca languageName: node linkType: hard @@ -8174,6 +8181,13 @@ __metadata: languageName: node linkType: hard +"@ungap/promise-all-settled@npm:1.1.2": + version: 1.1.2 + resolution: "@ungap/promise-all-settled@npm:1.1.2" + checksum: 08d37fdfa23a6fe8139f1305313562ebad973f3fac01bcce2773b2bda5bcb0146dfdcf3cb6a722cf0a5f2ca0bc56a827eac8f1e7b3beddc548f654addf1fc34c + languageName: node + linkType: hard + "@vue/compiler-core@npm:3.1.4": version: 3.1.4 resolution: "@vue/compiler-core@npm:3.1.4" @@ -9133,10 +9147,10 @@ __metadata: languageName: node linkType: hard -"ansi-colors@npm:3.2.3": - version: 3.2.3 - resolution: "ansi-colors@npm:3.2.3" - checksum: 018a92fbf8b143feb9e00559655072598902ff2cdfa07dbe24b933c70ae04845e3dda2c091ab128920fc50b3db06c3f09947f49fcb287d53beb6c5869b8bb32b +"ansi-colors@npm:4.1.1": + version: 4.1.1 + resolution: "ansi-colors@npm:4.1.1" + checksum: 138d04a51076cb085da0a7e2d000c5c0bb09f6e772ed5c65c53cb118d37f6c5f1637506d7155fb5f330f0abcf6f12fa2e489ac3f8cdab9da393bf1bb4f9a32b0 languageName: node linkType: hard @@ -9191,16 +9205,16 @@ __metadata: linkType: hard "ansi-regex@npm:^3.0.0": - version: 3.0.0 - resolution: "ansi-regex@npm:3.0.0" - checksum: 2ad11c416f81c39f5c65eafc88cf1d71aa91d76a2f766e75e457c2a3c43e8a003aadbf2966b61c497aa6a6940a36412486c975b3270cdfc3f413b69826189ec3 + version: 3.0.1 + resolution: "ansi-regex@npm:3.0.1" + checksum: 09daf180c5f59af9850c7ac1bd7fda85ba596cc8cbeb210826e90755f06c818af86d9fa1e6e8322fab2c3b9e9b03f56c537b42241139f824dd75066a1e7257cc languageName: node linkType: hard "ansi-regex@npm:^4.1.0": - version: 4.1.0 - resolution: "ansi-regex@npm:4.1.0" - checksum: 97aa4659538d53e5e441f5ef2949a3cffcb838e57aeaad42c4194e9d7ddb37246a6526c4ca85d3940a9d1e19b11cc2e114530b54c9d700c8baf163c31779baf8 + version: 4.1.1 + resolution: "ansi-regex@npm:4.1.1" + checksum: b1a6ee44cb6ecdabaa770b2ed500542714d4395d71c7e5c25baa631f680fb2ad322eb9ba697548d498a6fd366949fc8b5bfcf48d49a32803611f648005b01888 languageName: node linkType: hard @@ -9289,7 +9303,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.0, anymatch@npm:^3.0.3, anymatch@npm:^3.1.0, anymatch@npm:~3.1.1, anymatch@npm:~3.1.2": +"anymatch@npm:^3.0.0, anymatch@npm:^3.0.3, anymatch@npm:^3.1.0, anymatch@npm:~3.1.2": version: 3.1.2 resolution: "anymatch@npm:3.1.2" dependencies: @@ -11835,26 +11849,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.3.0": - version: 3.3.0 - resolution: "chokidar@npm:3.3.0" - dependencies: - anymatch: ~3.1.1 - braces: ~3.0.2 - fsevents: ~2.1.1 - glob-parent: ~5.1.0 - is-binary-path: ~2.1.0 - is-glob: ~4.0.1 - normalize-path: ~3.0.0 - readdirp: ~3.2.0 - dependenciesMeta: - fsevents: - optional: true - checksum: e9863256ebb29dbc5e58a7e2637439814beb63b772686cb9e94478312c24dcaf3d0570220c5e75ea29029f43b664f9956d87b716120d38cf755f32124f047e8e - languageName: node - linkType: hard - -"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.0, chokidar@npm:^3.4.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -12104,17 +12099,6 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^5.0.0": - version: 5.0.0 - resolution: "cliui@npm:5.0.0" - dependencies: - string-width: ^3.1.0 - strip-ansi: ^5.2.0 - wrap-ansi: ^5.1.0 - checksum: 0bb8779efe299b8f3002a73619eaa8add4081eb8d1c17bc4fedc6240557fb4eacdc08fe87c39b002eacb6cfc117ce736b362dbfd8bf28d90da800e010ee97df4 - languageName: node - linkType: hard - "cliui@npm:^6.0.0": version: 6.0.0 resolution: "cliui@npm:6.0.0" @@ -13482,15 +13466,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:3.2.6": - version: 3.2.6 - resolution: "debug@npm:3.2.6" - dependencies: - ms: ^2.1.1 - checksum: 07bc8b3a13ef3cfa6c06baf7871dfb174c291e5f85dbf566f086620c16b9c1a0e93bb8f1935ebbd07a683249e7e30286f2966e2ef461e8fd17b1b60732062d6b - languageName: node - linkType: hard - "debug@npm:3.X, debug@npm:^3.0.0, debug@npm:^3.1.0, debug@npm:^3.2.6, debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" @@ -13521,6 +13496,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:4.3.3": + version: 4.3.3 + resolution: "debug@npm:4.3.3" + dependencies: + ms: 2.1.2 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 14472d56fe4a94dbcfaa6dbed2dd3849f1d72ba78104a1a328047bb564643ca49df0224c3a17fa63533fd11dd3d4c8636cd861191232a2c6735af00cc2d4de16 + languageName: node + linkType: hard + "debug@npm:~3.1.0": version: 3.1.0 resolution: "debug@npm:3.1.0" @@ -13554,6 +13541,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^4.0.0": + version: 4.0.0 + resolution: "decamelize@npm:4.0.0" + checksum: b7d09b82652c39eead4d6678bb578e3bebd848add894b76d0f6b395bc45b2d692fb88d977e7cfb93c4ed6c119b05a1347cef261174916c2e75c0a8ca57da1809 + languageName: node + linkType: hard + "decimal.js@npm:^10.2.0, decimal.js@npm:^10.3.1": version: 10.4.0 resolution: "decimal.js@npm:10.4.0" @@ -14148,10 +14142,10 @@ __metadata: languageName: node linkType: hard -"diff@npm:3.5.0": - version: 3.5.0 - resolution: "diff@npm:3.5.0" - checksum: 00842950a6551e26ce495bdbce11047e31667deea546527902661f25cc2e73358967ebc78cf86b1a9736ec3e14286433225f9970678155753a6291c3bca5227b +"diff@npm:5.0.0, diff@npm:^5.0.0": + version: 5.0.0 + resolution: "diff@npm:5.0.0" + checksum: f19fe29284b633afdb2725c2a8bb7d25761ea54d321d8e67987ac851c5294be4afeab532bd84531e02583a3fe7f4014aa314a3eda84f5590e7a9e6b371ef3b46 languageName: node linkType: hard @@ -14162,13 +14156,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^5.0.0": - version: 5.0.0 - resolution: "diff@npm:5.0.0" - checksum: f19fe29284b633afdb2725c2a8bb7d25761ea54d321d8e67987ac851c5294be4afeab532bd84531e02583a3fe7f4014aa314a3eda84f5590e7a9e6b371ef3b46 - languageName: node - linkType: hard - "diffable-html@npm:^4.1.0": version: 4.1.0 resolution: "diffable-html@npm:4.1.0" @@ -14951,7 +14938,14 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:1.0.5, escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": +"escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 @@ -14965,13 +14959,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 - languageName: node - linkType: hard - "escodegen@npm:^1.11.1, escodegen@npm:^1.8.1, escodegen@npm:^1.9.0": version: 1.14.3 resolution: "escodegen@npm:1.14.3" @@ -15142,15 +15129,15 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-mocha@npm:^8.1.0": - version: 8.1.0 - resolution: "eslint-plugin-mocha@npm:8.1.0" +"eslint-plugin-mocha@npm:^10.1.0": + version: 10.1.0 + resolution: "eslint-plugin-mocha@npm:10.1.0" dependencies: - eslint-utils: ^2.1.0 - ramda: ^0.27.1 + eslint-utils: ^3.0.0 + rambda: ^7.1.0 peerDependencies: eslint: ">=7.0.0" - checksum: c3efc9482fbda003f15847ee581f57e68e2c223664c743ab0613488894c00dc6bb2e9cca52718b964aa6e241e6d74292fa324a6ad9b697f7046643d616bf860f + checksum: 67c063ba190fe8ab3186baaf800a375e9f16a17f69deaac2ea0d1825f6e4260f9a56bd510ceb2ffbe6644d7090beda0efbd2ab7824e4852ce2abee53a1086179 languageName: node linkType: hard @@ -15278,7 +15265,7 @@ __metadata: languageName: node linkType: hard -"eslint-utils@npm:^2.0.0, eslint-utils@npm:^2.1.0": +"eslint-utils@npm:^2.0.0": version: 2.1.0 resolution: "eslint-utils@npm:2.1.0" dependencies: @@ -17076,12 +17063,13 @@ __metadata: languageName: node linkType: hard -"find-up@npm:3.0.0, find-up@npm:^3.0.0": - version: 3.0.0 - resolution: "find-up@npm:3.0.0" +"find-up@npm:5.0.0, find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" dependencies: - locate-path: ^3.0.0 - checksum: 38eba3fe7a66e4bc7f0f5a1366dc25508b7cfc349f852640e3678d26ad9a6d7e2c43eff0a472287de4a9753ef58f066a0ea892a256fa3636ad51b3fe1e17fae9 + locate-path: ^6.0.0 + path-exists: ^4.0.0 + checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 languageName: node linkType: hard @@ -17104,6 +17092,15 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^3.0.0": + version: 3.0.0 + resolution: "find-up@npm:3.0.0" + dependencies: + locate-path: ^3.0.0 + checksum: 38eba3fe7a66e4bc7f0f5a1366dc25508b7cfc349f852640e3678d26ad9a6d7e2c43eff0a472287de4a9753ef58f066a0ea892a256fa3636ad51b3fe1e17fae9 + languageName: node + linkType: hard + "find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" @@ -17114,16 +17111,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^5.0.0": - version: 5.0.0 - resolution: "find-up@npm:5.0.0" - dependencies: - locate-path: ^6.0.0 - path-exists: ^4.0.0 - checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 - languageName: node - linkType: hard - "findup-sync@npm:^2.0.0": version: 2.0.0 resolution: "findup-sync@npm:2.0.0" @@ -17205,14 +17192,12 @@ __metadata: languageName: node linkType: hard -"flat@npm:^4.1.0": - version: 4.1.0 - resolution: "flat@npm:4.1.0" - dependencies: - is-buffer: ~2.0.3 +"flat@npm:^5.0.2": + version: 5.0.2 + resolution: "flat@npm:5.0.2" bin: flat: cli.js - checksum: 41a91335be78c5c16813672a6371871034763db85ed84b31926b132ebeb145d63cd05460e33e4197358ed6a862e2c25c01721c8b2b20d292ff1e166795655f09 + checksum: 12a1536ac746db74881316a181499a78ef953632ddd28050b7a3a43c62ef5462e3357c8c29d76072bb635f147f7a9a1f0c02efef6b4be28f8db62ceb3d5c7f5d languageName: node linkType: hard @@ -17601,16 +17586,6 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:~2.1.1": - version: 2.1.3 - resolution: "fsevents@npm:2.1.3" - dependencies: - node-gyp: latest - checksum: b5ec0516b44d75b60af5c01ff80a80cd995d175e4640d2a92fbabd02991dd664d76b241b65feef0775c23d531c3c74742c0fbacd6205af812a9c3cef59f04292 - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@patch:fsevents@^1.2.7#~builtin": version: 1.2.9 resolution: "fsevents@patch:fsevents@npm%3A1.2.9#~builtin::version=1.2.9&hash=18f3a7" @@ -17630,15 +17605,6 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@~2.1.1#~builtin": - version: 2.1.3 - resolution: "fsevents@patch:fsevents@npm%3A2.1.3#~builtin::version=2.1.3&hash=18f3a7" - dependencies: - node-gyp: latest - conditions: os=darwin - languageName: node - linkType: hard - "ftp@npm:^0.3.10": version: 0.3.10 resolution: "ftp@npm:0.3.10" @@ -18035,7 +18001,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.1, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.0, glob-parent@npm:~5.1.2": +"glob-parent@npm:^5.1.1, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -18110,9 +18076,9 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.1.3": - version: 7.1.3 - resolution: "glob@npm:7.1.3" +"glob@npm:7.2.0, glob@npm:^7.0.0, glob@npm:^7.0.3, glob@npm:^7.1.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": + version: 7.2.0 + resolution: "glob@npm:7.2.0" dependencies: fs.realpath: ^1.0.0 inflight: ^1.0.4 @@ -18120,7 +18086,7 @@ __metadata: minimatch: ^3.0.4 once: ^1.3.0 path-is-absolute: ^1.0.0 - checksum: d72a834a393948d6c4a5cacc6a29fe5fe190e1cd134e55dfba09aee0be6fe15be343e96d8ec43558ab67ff8af28e4420c7f63a4d4db1c779e515015e9c318616 + checksum: 78a8ea942331f08ed2e055cb5b9e40fe6f46f579d7fd3d694f3412fe5db23223d29b7fee1575440202e9a7ff9a72ab106a39fee39934c7bedafe5e5f8ae20134 languageName: node linkType: hard @@ -18138,20 +18104,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.0.3, glob@npm:^7.1.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": - version: 7.2.0 - resolution: "glob@npm:7.2.0" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.0.4 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 78a8ea942331f08ed2e055cb5b9e40fe6f46f579d7fd3d694f3412fe5db23223d29b7fee1575440202e9a7ff9a72ab106a39fee39934c7bedafe5e5f8ae20134 - languageName: node - linkType: hard - "glob@npm:^8.0.1": version: 8.0.3 resolution: "glob@npm:8.0.3" @@ -19998,7 +19950,7 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^2.0.0, is-buffer@npm:^2.0.5, is-buffer@npm:~2.0.3": +"is-buffer@npm:^2.0.0, is-buffer@npm:^2.0.5": version: 2.0.5 resolution: "is-buffer@npm:2.0.5" checksum: 764c9ad8b523a9f5a32af29bdf772b08eb48c04d2ad0a7240916ac2688c983bf5f8504bf25b35e66240edeb9d9085461f9b5dae1f3d2861c6b06a65fe983de42 @@ -20483,7 +20435,7 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^2.0.0": +"is-plain-obj@npm:^2.0.0, is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" checksum: cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa @@ -22102,27 +22054,26 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:3.13.1": - version: 3.13.1 - resolution: "js-yaml@npm:3.13.1" +"js-yaml@npm:3.14.0": + version: 3.14.0 + resolution: "js-yaml@npm:3.14.0" dependencies: argparse: ^1.0.7 esprima: ^4.0.0 bin: js-yaml: bin/js-yaml.js - checksum: 7511b764abb66d8aa963379f7d2a404f078457d106552d05a7b556d204f7932384e8477513c124749fa2de52eb328961834562bd09924902c6432e40daa408bc + checksum: a1a47c912ba20956f96cb0998dea2e74c7f7129d831fe33d3c5a16f3f83712ce405172a8dd1c26bf2b3ad74b54016d432ff727928670ae5a50a57a677c387949 languageName: node linkType: hard -"js-yaml@npm:3.14.0": - version: 3.14.0 - resolution: "js-yaml@npm:3.14.0" +"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" dependencies: - argparse: ^1.0.7 - esprima: ^4.0.0 + argparse: ^2.0.1 bin: js-yaml: bin/js-yaml.js - checksum: a1a47c912ba20956f96cb0998dea2e74c7f7129d831fe33d3c5a16f3f83712ce405172a8dd1c26bf2b3ad74b54016d432ff727928670ae5a50a57a677c387949 + checksum: c7830dfd456c3ef2c6e355cc5a92e6700ceafa1d14bba54497b34a99f0376cecbb3e9ac14d3e5849b426d5a5140709a66237a8c991c675431271c4ce5504151a languageName: node linkType: hard @@ -22138,17 +22089,6 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.0": - version: 4.1.0 - resolution: "js-yaml@npm:4.1.0" - dependencies: - argparse: ^2.0.1 - bin: - js-yaml: bin/js-yaml.js - checksum: c7830dfd456c3ef2c6e355cc5a92e6700ceafa1d14bba54497b34a99f0376cecbb3e9ac14d3e5849b426d5a5140709a66237a8c991c675431271c4ce5504151a - languageName: node - linkType: hard - "jsan@npm:^3.1.13": version: 3.1.13 resolution: "jsan@npm:3.1.13" @@ -23447,12 +23387,13 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:3.0.0": - version: 3.0.0 - resolution: "log-symbols@npm:3.0.0" +"log-symbols@npm:4.1.0, log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" dependencies: - chalk: ^2.4.2 - checksum: f2322e1452d819050b11aad247660e1494f8b2219d40a964af91d5f9af1a90636f1b3d93f2952090e42af07cc5550aecabf6c1d8ec1181207e95cb66ba112361 + chalk: ^4.1.0 + is-unicode-supported: ^0.1.0 + checksum: fce1497b3135a0198803f9f07464165e9eb83ed02ceb2273930a6f8a508951178d8cf4f0378e9d28300a2ed2bc49050995d2bd5f53ab716bb15ac84d58c6ef74 languageName: node linkType: hard @@ -23465,16 +23406,6 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:^4.0.0, log-symbols@npm:^4.1.0": - version: 4.1.0 - resolution: "log-symbols@npm:4.1.0" - dependencies: - chalk: ^4.1.0 - is-unicode-supported: ^0.1.0 - checksum: fce1497b3135a0198803f9f07464165e9eb83ed02ceb2273930a6f8a508951178d8cf4f0378e9d28300a2ed2bc49050995d2bd5f53ab716bb15ac84d58c6ef74 - languageName: node - linkType: hard - "loglevel@npm:^1.8.0, loglevel@npm:^1.8.1": version: 1.8.1 resolution: "loglevel@npm:1.8.1" @@ -24219,6 +24150,7 @@ __metadata: "@ethersproject/providers": ^5.7.2 "@formatjs/intl-relativetimeformat": ^5.2.6 "@fortawesome/fontawesome-free": ^5.13.0 + "@json-rpc-specification/meta-schema": ^1.0.6 "@keystonehq/bc-ur-registry-eth": ^0.12.1 "@keystonehq/metamask-airgapped-keyring": ^0.6.1 "@lavamoat/allow-scripts": ^2.0.3 @@ -24227,7 +24159,7 @@ __metadata: "@material-ui/core": ^4.11.0 "@metamask/address-book-controller": ^2.0.0 "@metamask/announcement-controller": ^3.0.0 - "@metamask/approval-controller": ^2.0.0 + "@metamask/approval-controller": ^2.1.0 "@metamask/assets-controllers": ^5.0.0 "@metamask/auto-changelog": ^2.1.0 "@metamask/base-controller": ^2.0.0 @@ -24371,7 +24303,7 @@ __metadata: eslint-plugin-import: ^2.22.1 eslint-plugin-jest: ^26.6.0 eslint-plugin-jsdoc: ^39.3.3 - eslint-plugin-mocha: ^8.1.0 + eslint-plugin-mocha: ^10.1.0 eslint-plugin-node: ^11.1.0 eslint-plugin-prettier: ^4.2.1 eslint-plugin-react: ^7.23.1 @@ -24446,7 +24378,7 @@ __metadata: loose-envify: ^1.4.0 luxon: ^3.2.1 madge: ^5.0.1 - mocha: ^7.2.0 + mocha: ^9.2.2 mockttp: ^2.6.0 nanoid: ^2.1.6 nock: ^13.2.9 @@ -24751,6 +24683,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:4.2.1": + version: 4.2.1 + resolution: "minimatch@npm:4.2.1" + dependencies: + brace-expansion: ^1.1.7 + checksum: 2b1514e3d0f29a549912f0db7ae7b82c5cab4a8f2dd0369f1c6451a325b3f12b2cf473c95873b6157bb8df183d6cf6db82ff03614b6adaaf1d7e055beccdfd01 + languageName: node + linkType: hard + "minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -24921,17 +24862,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:0.5.5": - version: 0.5.5 - resolution: "mkdirp@npm:0.5.5" - dependencies: - minimist: ^1.2.5 - bin: - mkdirp: bin/cmd.js - checksum: 3bce20ea525f9477befe458ab85284b0b66c8dc3812f94155af07c827175948cdd8114852ac6c6d82009b13c1048c37f6d98743eb019651ee25c39acc8aabe7d - languageName: node - linkType: hard - "mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.3, mkdirp@npm:^0.5.4, mkdirp@npm:^0.5.5, mkdirp@npm:^0.5.6": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -24952,38 +24882,38 @@ __metadata: languageName: node linkType: hard -"mocha@npm:^7.2.0": - version: 7.2.0 - resolution: "mocha@npm:7.2.0" +"mocha@npm:^9.2.2": + version: 9.2.2 + resolution: "mocha@npm:9.2.2" dependencies: - ansi-colors: 3.2.3 + "@ungap/promise-all-settled": 1.1.2 + ansi-colors: 4.1.1 browser-stdout: 1.3.1 - chokidar: 3.3.0 - debug: 3.2.6 - diff: 3.5.0 - escape-string-regexp: 1.0.5 - find-up: 3.0.0 - glob: 7.1.3 + chokidar: 3.5.3 + debug: 4.3.3 + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 growl: 1.10.5 he: 1.2.0 - js-yaml: 3.13.1 - log-symbols: 3.0.0 - minimatch: 3.0.4 - mkdirp: 0.5.5 - ms: 2.1.1 - node-environment-flags: 1.0.6 - object.assign: 4.1.0 - strip-json-comments: 2.0.1 - supports-color: 6.0.0 - which: 1.3.1 - wide-align: 1.1.3 - yargs: 13.3.2 - yargs-parser: 13.1.2 - yargs-unparser: 1.6.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 4.2.1 + ms: 2.1.3 + nanoid: 3.3.1 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + which: 2.0.2 + workerpool: 6.2.0 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 bin: _mocha: bin/_mocha mocha: bin/mocha - checksum: d098484fe1b165bb964fdbf6b88b256c71fead47575ca7c5bcf8ed07db0dcff41905f6d2f0a05111a0441efaef9d09241a8cc1ddf7961056b28984ec63ba2874 + checksum: 4d5ca4ce33fc66627e63acdf09a634e2358c9a00f61de7788b1091b6aad430da04f97f9ecb82d56dc034b623cb833b65576136fd010d77679c03fcea5bc1e12d languageName: node linkType: hard @@ -25129,7 +25059,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -25302,6 +25232,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:3.3.1": + version: 3.3.1 + resolution: "nanoid@npm:3.3.1" + bin: + nanoid: bin/nanoid.cjs + checksum: 4ef0969e1bbe866fc223eb32276cbccb0961900bfe79104fa5abe34361979dead8d0e061410a5c03bc3d47455685adf32c09d6f27790f4a6898fb51f7df7ec86 + languageName: node + linkType: hard + "nanoid@npm:^2.0.0, nanoid@npm:^2.1.6": version: 2.1.11 resolution: "nanoid@npm:2.1.11" @@ -25502,16 +25441,6 @@ __metadata: languageName: node linkType: hard -"node-environment-flags@npm:1.0.6": - version: 1.0.6 - resolution: "node-environment-flags@npm:1.0.6" - dependencies: - object.getownpropertydescriptors: ^2.0.3 - semver: ^5.7.0 - checksum: 268139ed0f7fabdca346dcb26931300ec7a1dc54a58085a849e5c78a82b94967f55df40177a69d4e819da278d98686d5c4fd49ab0d7bcff16fda25b6fffc4ca3 - languageName: node - linkType: hard - "node-fetch@npm:2.6.7, node-fetch@npm:^2, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:~2.6.1": version: 2.6.7 resolution: "node-fetch@npm:2.6.7" @@ -26045,7 +25974,7 @@ __metadata: languageName: node linkType: hard -"object-keys@npm:^1.0.11, object-keys@npm:^1.1.1": +"object-keys@npm:^1.1.1": version: 1.1.1 resolution: "object-keys@npm:1.1.1" checksum: b363c5e7644b1e1b04aa507e88dcb8e3a2f52b6ffd0ea801e4c7a62d5aa559affe21c55a07fd4b1fd55fc03a33c610d73426664b20032405d7b92a1414c34d6a @@ -26068,18 +25997,6 @@ __metadata: languageName: node linkType: hard -"object.assign@npm:4.1.0": - version: 4.1.0 - resolution: "object.assign@npm:4.1.0" - dependencies: - define-properties: ^1.1.2 - function-bind: ^1.1.1 - has-symbols: ^1.0.0 - object-keys: ^1.0.11 - checksum: 648a9a463580bf48332d9a49a76fede2660ab1ee7104d9459b8a240562246da790b4151c3c073f28fda31c1fdc555d25a1d871e72be403e997e4468c91f4801f - languageName: node - linkType: hard - "object.assign@npm:^4.0.4, object.assign@npm:^4.1.0, object.assign@npm:^4.1.2, object.assign@npm:^4.1.4": version: 4.1.4 resolution: "object.assign@npm:4.1.4" @@ -28441,6 +28358,13 @@ __metadata: languageName: node linkType: hard +"rambda@npm:^7.1.0": + version: 7.5.0 + resolution: "rambda@npm:7.5.0" + checksum: ad608a9a4160d0b6b0921047cea1329276bf239ff58d439135288712dcdbbf0df47c76591843ad249d89e7c5a9109ce86fe099aa54aef0dc0aa92a9b4dd1b8eb + languageName: node + linkType: hard + "ramda@npm:^0.21.0": version: 0.21.0 resolution: "ramda@npm:0.21.0" @@ -28448,13 +28372,6 @@ __metadata: languageName: node linkType: hard -"ramda@npm:^0.27.1": - version: 0.27.1 - resolution: "ramda@npm:0.27.1" - checksum: 31a0c0ef739b2525d7615f84cbb5d3cb89ee0c795469b711f729ea1d8df0dccc3cd75d3717a1e9742d42315ce86435680b7c87743eb7618111c60c144a5b8059 - languageName: node - linkType: hard - "randomatic@npm:^3.0.0": version: 3.0.0 resolution: "randomatic@npm:3.0.0" @@ -29208,15 +29125,6 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:~3.2.0": - version: 3.2.0 - resolution: "readdirp@npm:3.2.0" - dependencies: - picomatch: ^2.0.4 - checksum: 0456a4465a13eb5eaf40f0e0836b1bc6b9ebe479b48ba6f63a738b127a1990fb7b38f3ec4b4b6052f9230f976bc0558f12812347dc6b42ce4d548cfe82a9b6f3 - languageName: node - linkType: hard - "real-require@npm:^0.1.0": version: 0.1.0 resolution: "real-require@npm:0.1.0" @@ -30746,7 +30654,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.0.3, semver@npm:^5.1.0, semver@npm:^5.3.0, semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.6.0, semver@npm:^5.7.0": +"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.0.3, semver@npm:^5.1.0, semver@npm:^5.3.0, semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.6.0": version: 5.7.1 resolution: "semver@npm:5.7.1" bin: @@ -30835,6 +30743,15 @@ __metadata: languageName: node linkType: hard +"serialize-javascript@npm:6.0.0, serialize-javascript@npm:^6.0.0": + version: 6.0.0 + resolution: "serialize-javascript@npm:6.0.0" + dependencies: + randombytes: ^2.1.0 + checksum: 56f90b562a1bdc92e55afb3e657c6397c01a902c588c0fe3d4c490efdcc97dcd2a3074ba12df9e94630f33a5ce5b76a74784a7041294628a6f4306e0ec84bf93 + languageName: node + linkType: hard + "serialize-javascript@npm:^4.0.0": version: 4.0.0 resolution: "serialize-javascript@npm:4.0.0" @@ -30853,15 +30770,6 @@ __metadata: languageName: node linkType: hard -"serialize-javascript@npm:^6.0.0": - version: 6.0.0 - resolution: "serialize-javascript@npm:6.0.0" - dependencies: - randombytes: ^2.1.0 - checksum: 56f90b562a1bdc92e55afb3e657c6397c01a902c588c0fe3d4c490efdcc97dcd2a3074ba12df9e94630f33a5ce5b76a74784a7041294628a6f4306e0ec84bf93 - languageName: node - linkType: hard - "serve-favicon@npm:^2.5.0": version: 2.5.0 resolution: "serve-favicon@npm:2.5.0" @@ -31904,7 +31812,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^1.0.2 || 2, string-width@npm:^2.0.0, string-width@npm:^2.1.1": +"string-width@npm:^2.0.0, string-width@npm:^2.1.1": version: 2.1.1 resolution: "string-width@npm:2.1.1" dependencies: @@ -31914,7 +31822,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^3.0.0, string-width@npm:^3.1.0": +"string-width@npm:^3.0.0": version: 3.1.0 resolution: "string-width@npm:3.1.0" dependencies: @@ -32072,7 +31980,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^5.0.0, strip-ansi@npm:^5.1.0, strip-ansi@npm:^5.2.0": +"strip-ansi@npm:^5.1.0": version: 5.2.0 resolution: "strip-ansi@npm:5.2.0" dependencies: @@ -32201,20 +32109,20 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:2.0.1, strip-json-comments@npm:^2.0.0, strip-json-comments@npm:~2.0.1": - version: 2.0.1 - resolution: "strip-json-comments@npm:2.0.1" - checksum: 1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 - languageName: node - linkType: hard - -"strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 languageName: node linkType: hard +"strip-json-comments@npm:^2.0.0, strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 + languageName: node + linkType: hard + "strip-outer@npm:^1.0.1": version: 1.0.1 resolution: "strip-outer@npm:1.0.1" @@ -32473,12 +32381,12 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:6.0.0": - version: 6.0.0 - resolution: "supports-color@npm:6.0.0" +"supports-color@npm:8.1.1, supports-color@npm:^8.0.0, supports-color@npm:^8.1.0": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" dependencies: - has-flag: ^3.0.0 - checksum: 005b4a7e5d78a9a703454f5b7da34336b82825747724d1f3eefea6c3956afcb33b79b31854a93cef0fc1f2449919ae952f79abbfd09a5b5b43ecd26407d3a3a1 + has-flag: ^4.0.0 + checksum: c052193a7e43c6cdc741eb7f378df605636e01ad434badf7324f17fb60c69a880d8d8fcdcb562cf94c2350e57b937d7425ab5b8326c67c2adc48f7c87c1db406 languageName: node linkType: hard @@ -32509,15 +32417,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0, supports-color@npm:^8.1.0": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: ^4.0.0 - checksum: c052193a7e43c6cdc741eb7f378df605636e01ad434badf7324f17fb60c69a880d8d8fcdcb562cf94c2350e57b937d7425ab5b8326c67c2adc48f7c87c1db406 - languageName: node - linkType: hard - "supports-hyperlinks@npm:^2.0.0": version: 2.2.0 resolution: "supports-hyperlinks@npm:2.2.0" @@ -34566,14 +34465,14 @@ __metadata: linkType: hard "vm2@npm:^3.9.3": - version: 3.9.15 - resolution: "vm2@npm:3.9.15" + version: 3.9.16 + resolution: "vm2@npm:3.9.16" dependencies: acorn: ^8.7.0 acorn-walk: ^8.2.0 bin: vm2: bin/vm2 - checksum: 1df70d5a88173651c0062901aba67e5edfeeb3f699fe6c305f5efb6a5a7391e5724cbf98a6516600b65016c6824dc07cc79947ea4222f8537ae1d9ce0b730ad7 + checksum: 646b45dca721acb3c8e4ae0742129f13612972387911c2475f3c06ac2b4232000cab0bdaaa65d97d6ea8dc70880e039542618b1b3d04adea79cd94803cbc4ab3 languageName: node linkType: hard @@ -35169,18 +35068,7 @@ __metadata: languageName: node linkType: hard -"which@npm:1.3.1, which@npm:^1.2.12, which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": - version: 1.3.1 - resolution: "which@npm:1.3.1" - dependencies: - isexe: ^2.0.0 - bin: - which: ./bin/which - checksum: f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04 - languageName: node - linkType: hard - -"which@npm:^2.0.1, which@npm:^2.0.2": +"which@npm:2.0.2, which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" dependencies: @@ -35191,12 +35079,14 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:1.1.3": - version: 1.1.3 - resolution: "wide-align@npm:1.1.3" +"which@npm:^1.2.12, which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": + version: 1.3.1 + resolution: "which@npm:1.3.1" dependencies: - string-width: ^1.0.2 || 2 - checksum: d09c8012652a9e6cab3e82338d1874a4d7db2ad1bd19ab43eb744acf0b9b5632ec406bdbbbb970a8f4771a7d5ef49824d038ba70aa884e7723f5b090ab87134d + isexe: ^2.0.0 + bin: + which: ./bin/which + checksum: f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04 languageName: node linkType: hard @@ -35277,6 +35167,13 @@ __metadata: languageName: node linkType: hard +"workerpool@npm:6.2.0": + version: 6.2.0 + resolution: "workerpool@npm:6.2.0" + checksum: 3493b4f0ef979a23d2c1583d7ef85f62fc9463cc02f82829d3e7e663b517f8ae9707da0249b382e46ac58986deb0ca2232ee1081713741211bda9254b429c9bb + languageName: node + linkType: hard + "wrap-ansi@npm:^2.0.0": version: 2.1.0 resolution: "wrap-ansi@npm:2.1.0" @@ -35287,17 +35184,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^5.1.0": - version: 5.1.0 - resolution: "wrap-ansi@npm:5.1.0" - dependencies: - ansi-styles: ^3.2.0 - string-width: ^3.0.0 - strip-ansi: ^5.0.0 - checksum: 9b48c862220e541eb0daa22661b38b947973fc57054e91be5b0f2dcc77741a6875ccab4ebe970a394b4682c8dfc17e888266a105fb8b0a9b23c19245e781ceae - languageName: node - linkType: hard - "wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -35577,13 +35463,10 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:13.1.2, yargs-parser@npm:^13.1.2": - version: 13.1.2 - resolution: "yargs-parser@npm:13.1.2" - dependencies: - camelcase: ^5.0.0 - decamelize: ^1.2.0 - checksum: c8bb6f44d39a4acd94462e96d4e85469df865de6f4326e0ab1ac23ae4a835e5dd2ddfe588317ebf80c3a7e37e741bd5cb0dc8d92bcc5812baefb7df7c885e86b +"yargs-parser@npm:20.2.4, yargs-parser@npm:^20.2.2": + version: 20.2.4 + resolution: "yargs-parser@npm:20.2.4" + checksum: d251998a374b2743a20271c2fd752b9fbef24eb881d53a3b99a7caa5e8227fcafd9abf1f345ac5de46435821be25ec12189a11030c12ee6481fef6863ed8b924 languageName: node linkType: hard @@ -35617,13 +35500,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2": - version: 20.2.4 - resolution: "yargs-parser@npm:20.2.4" - checksum: d251998a374b2743a20271c2fd752b9fbef24eb881d53a3b99a7caa5e8227fcafd9abf1f345ac5de46435821be25ec12189a11030c12ee6481fef6863ed8b924 - languageName: node - linkType: hard - "yargs-parser@npm:^21.0.0": version: 21.0.1 resolution: "yargs-parser@npm:21.0.1" @@ -35631,32 +35507,30 @@ __metadata: languageName: node linkType: hard -"yargs-unparser@npm:1.6.0": - version: 1.6.0 - resolution: "yargs-unparser@npm:1.6.0" +"yargs-unparser@npm:2.0.0": + version: 2.0.0 + resolution: "yargs-unparser@npm:2.0.0" dependencies: - flat: ^4.1.0 - lodash: ^4.17.15 - yargs: ^13.3.0 - checksum: ca662bb94af53d816d47f2162f0a1d135783f09de9fd47645a5cb18dd25532b0b710432b680d2c065ff45de122ba4a96433c41595fa7bfcc08eb12e889db95c1 + camelcase: ^6.0.0 + decamelize: ^4.0.0 + flat: ^5.0.2 + is-plain-obj: ^2.1.0 + checksum: 68f9a542c6927c3768c2f16c28f71b19008710abd6b8f8efbac6dcce26bbb68ab6503bed1d5994bdbc2df9a5c87c161110c1dfe04c6a3fe5c6ad1b0e15d9a8a3 languageName: node linkType: hard -"yargs@npm:13.3.2, yargs@npm:^13.3.0": - version: 13.3.2 - resolution: "yargs@npm:13.3.2" +"yargs@npm:16.2.0, yargs@npm:^16.0.0, yargs@npm:^16.1.0, yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" dependencies: - cliui: ^5.0.0 - find-up: ^3.0.0 - get-caller-file: ^2.0.1 + cliui: ^7.0.2 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 require-directory: ^2.1.1 - require-main-filename: ^2.0.0 - set-blocking: ^2.0.0 - string-width: ^3.0.0 - which-module: ^2.0.0 - y18n: ^4.0.0 - yargs-parser: ^13.1.2 - checksum: 75c13e837eb2bb25717957ba58d277e864efc0cca7f945c98bdf6477e6ec2f9be6afa9ed8a876b251a21423500c148d7b91e88dee7adea6029bdec97af1ef3e8 + string-width: ^4.2.0 + y18n: ^5.0.5 + yargs-parser: ^20.2.2 + checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 languageName: node linkType: hard @@ -35694,21 +35568,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^16.0.0, yargs@npm:^16.1.0, yargs@npm:^16.2.0": - version: 16.2.0 - resolution: "yargs@npm:16.2.0" - dependencies: - cliui: ^7.0.2 - escalade: ^3.1.1 - get-caller-file: ^2.0.5 - require-directory: ^2.1.1 - string-width: ^4.2.0 - y18n: ^5.0.5 - yargs-parser: ^20.2.2 - checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 - languageName: node - linkType: hard - "yargs@npm:^17.0.1, yargs@npm:^17.3.1": version: 17.5.1 resolution: "yargs@npm:17.5.1"