1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Merge remote-tracking branch 'upstream/develop' into minimal

This commit is contained in:
Matthias Kretschmann 2023-04-15 14:40:50 +01:00
commit edd6d489ba
Signed by: m
GPG Key ID: 606EEEF3C479A91F
189 changed files with 9209 additions and 4088 deletions

View File

@ -266,7 +266,8 @@ module.exports = {
'**/__snapshots__/*.snap',
'app/scripts/controllers/app-state.test.js',
'app/scripts/controllers/network/**/*.test.js',
'app/scripts/controllers/network/provider-api-tests/*.js',
'app/scripts/controllers/network/**/*.test.ts',
'app/scripts/controllers/network/provider-api-tests/*.ts',
'app/scripts/controllers/permissions/**/*.test.js',
'app/scripts/lib/**/*.test.js',
'app/scripts/migrations/*.test.js',

View File

@ -606,13 +606,6 @@ const state = {
rpcUrl: '',
chainId: '0x5',
},
previousProviderStore: {
type: 'goerli',
ticker: 'ETH',
nickname: '',
rpcUrl: '',
chainId: '0x5',
},
network: '5',
accounts: {
'0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': {

View File

@ -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"
},
@ -815,7 +822,7 @@
"message": "Contract deployment"
},
"contractDescription": {
"message": "To protect yourself against scammers, take a moment to verify contract details."
"message": "To protect yourself against scammers, take a moment to verify third-party details."
},
"contractInteraction": {
"message": "Contract interaction"
@ -830,10 +837,10 @@
"message": "Contract requesting signature"
},
"contractRequestingSpendingCap": {
"message": "Contract requesting spending cap"
"message": "Third party requesting spending cap"
},
"contractTitle": {
"message": "Contract details"
"message": "Third-party details"
},
"contractToken": {
"message": "Token contract"
@ -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"
},
@ -1808,14 +1830,14 @@
"message": "Your initial transaction was confirmed by the network. Click OK to go back."
},
"inputLogicEmptyState": {
"message": "Only enter a number that you're comfortable with the contract spending now or in the future. You can always increase the spending cap later."
"message": "Only enter a number that you're comfortable with the third party spending now or in the future. You can always increase the spending cap later."
},
"inputLogicEqualOrSmallerNumber": {
"message": "This allows the contract to spend $1 from your current balance.",
"message": "This allows the third party to spend $1 from your current balance.",
"description": "$1 is the current token balance in the account and the name of the current token"
},
"inputLogicHigherNumber": {
"message": "This allows the contract to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap."
"message": "This allows the third party to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap."
},
"insightsFromSnap": {
"message": "Insights from $1",
@ -2486,6 +2508,9 @@
"message": "OpenSea is the first provider for this feature. More providers coming soon!",
"description": "Description of a notification in the 'See What's New' popup. Describes Opensea Security Provider feature."
},
"notifications18Title": {
"message": "Stay safe with security alerts"
},
"notifications19ActionText": {
"message": "Enable NFT autodetection"
},
@ -3328,7 +3353,7 @@
"description": "$1 is a token symbol"
},
"revokeSpendingCapTooltipText": {
"message": "This contract will be unable to spend any more of your current or future tokens."
"message": "This third party will be unable to spend any more of your current or future tokens."
},
"rpcUrl": {
"message": "New RPC URL"
@ -4292,6 +4317,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"
},
@ -4660,7 +4694,7 @@
"message": "Username"
},
"verifyContractDetails": {
"message": "Verify contract details"
"message": "Verify third-party details"
},
"verifyThisTokenDecimalOn": {
"message": "Token decimal can be found on $1",
@ -4746,7 +4780,7 @@
"message": "Warning"
},
"warningTooltipText": {
"message": "$1 The contract could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.",
"message": "$1 The third party could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.",
"description": "$1 is a warning icon with text 'Be careful' in 'warning' colour"
},
"weak": {

View File

@ -80,7 +80,6 @@ log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info');
const platform = new ExtensionPlatform();
const notificationManager = new NotificationManager();
global.METAMASK_NOTIFIER = notificationManager;
let popupIsOpen = false;
let notificationIsOpen = false;
@ -209,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).
@ -688,7 +688,7 @@ export function setupController(
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
updateBadge,
);
controller.encryptionPublicKeyManager.on(
controller.encryptionPublicKeyController.hub.on(
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
updateBadge,
);
@ -727,20 +727,13 @@ export function setupController(
}
function getUnapprovedTransactionCount() {
const unapprovedTxCount = controller.txController.getUnapprovedTxCount();
const { unapprovedDecryptMsgCount } = controller.decryptMessageManager;
const { unapprovedEncryptionPublicKeyMsgCount } =
controller.encryptionPublicKeyManager;
const pendingApprovalCount =
controller.approvalController.getTotalApprovalCount();
const waitingForUnlockCount =
controller.appStateController.waitingForUnlock.length;
return (
unapprovedTxCount +
unapprovedDecryptMsgCount +
unapprovedEncryptionPublicKeyMsgCount +
pendingApprovalCount +
waitingForUnlockCount
unapprovedDecryptMsgCount + pendingApprovalCount + waitingForUnlockCount
);
}
@ -770,14 +763,9 @@ export function setupController(
REJECT_NOTIFICATION_CLOSE,
),
);
controller.encryptionPublicKeyManager.messages
.filter((msg) => msg.status === 'unapproved')
.forEach((tx) =>
controller.encryptionPublicKeyManager.rejectMsg(
tx.id,
REJECT_NOTIFICATION_CLOSE,
),
);
controller.encryptionPublicKeyController.rejectUnapproved(
REJECT_NOTIFICATION_CLOSE,
);
// Finally, resolve snap dialog approvals on Flask and reject all the others managed by the ApprovalController.
Object.values(controller.approvalController.state.pendingApprovals).forEach(

View File

@ -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();
}
/**
@ -144,6 +152,37 @@ export default class AppStateController extends EventEmitter {
});
}
/**
* Record that the user has been shown the recovery phrase reminder.
*/
setRecoveryPhraseReminderHasBeenShown() {
this.store.updateState({
recoveryPhraseReminderHasBeenShown: true,
});
}
/**
* Record the timestamp of the last time the user has seen the recovery phrase reminder
*
* @param {number} lastShown - timestamp when user was last shown the reminder.
*/
setRecoveryPhraseReminderLastShown(lastShown) {
this.store.updateState({
recoveryPhraseReminderLastShown: lastShown,
});
}
/**
* 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
*
@ -349,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;
}
}

View File

@ -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().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().outdatedBrowserWarningLastShown,
).toStrictEqual(date);
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,
);
});
});
});

View File

@ -13,7 +13,7 @@ import { convertHexToDecimal } from '@metamask/controller-utils';
import { NETWORK_TYPES } from '../../../shared/constants/network';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import DetectTokensController from './detect-tokens';
import NetworkController, { NetworkControllerEventTypes } from './network';
import { NetworkController, NetworkControllerEventType } from './network';
import PreferencesController from './preferences';
describe('DetectTokensController', function () {
@ -248,7 +248,7 @@ describe('DetectTokensController', function () {
),
onNetworkStateChange: (cb) =>
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
() => {
const networkState = network.store.getState();
const modifiedNetworkState = {

View File

@ -0,0 +1,400 @@
import { EncryptionPublicKeyManager } from '@metamask/message-manager';
import {
AbstractMessage,
OriginalRequest,
} from '@metamask/message-manager/dist/AbstractMessageManager';
import { KeyringType } from '../../../shared/constants/keyring';
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
import EncryptionPublicKeyController, {
EncryptionPublicKeyControllerMessenger,
EncryptionPublicKeyControllerOptions,
} from './encryption-public-key';
jest.mock('@metamask/message-manager', () => ({
EncryptionPublicKeyManager: jest.fn(),
}));
const messageIdMock = '123';
const messageIdMock2 = '456';
const stateMock = { test: 123 };
const addressMock = '0xc38bf1ad06ef69f0c04e29dbeb4152b4175f0a8d';
const publicKeyMock = '32762347862378feb87123781623a=';
const keyringMock = { type: KeyringType.hdKeyTree };
const messageParamsMock = {
from: addressMock,
origin: 'http://test.com',
data: addressMock,
metamaskId: messageIdMock,
};
const messageMock = {
id: messageIdMock,
time: 123,
status: 'unapproved',
type: 'testType',
rawSig: undefined,
} as any as AbstractMessage;
const coreMessageMock = {
...messageMock,
messageParams: messageParamsMock,
};
const stateMessageMock = {
...messageMock,
msgParams: addressMock,
origin: messageParamsMock.origin,
};
const requestMock = {
origin: 'http://test2.com',
} as OriginalRequest;
const createMessengerMock = () =>
({
registerActionHandler: jest.fn(),
publish: jest.fn(),
call: jest.fn(),
} as any as jest.Mocked<EncryptionPublicKeyControllerMessenger>);
const createEncryptionPublicKeyManagerMock = <T>() =>
({
getUnapprovedMessages: jest.fn(),
getUnapprovedMessagesCount: jest.fn(),
addUnapprovedMessageAsync: jest.fn(),
approveMessage: jest.fn(),
setMessageStatusAndResult: jest.fn(),
rejectMessage: jest.fn(),
subscribe: jest.fn(),
update: jest.fn(),
hub: {
on: jest.fn(),
},
} as any as jest.Mocked<T>);
const createKeyringControllerMock = () => ({
getKeyringForAccount: jest.fn(),
getEncryptionPublicKey: jest.fn(),
});
describe('EncryptionPublicKeyController', () => {
let encryptionPublicKeyController: EncryptionPublicKeyController;
const encryptionPublicKeyManagerConstructorMock =
EncryptionPublicKeyManager as jest.MockedClass<
typeof EncryptionPublicKeyManager
>;
const encryptionPublicKeyManagerMock =
createEncryptionPublicKeyManagerMock<EncryptionPublicKeyManager>();
const messengerMock = createMessengerMock();
const keyringControllerMock = createKeyringControllerMock();
const getStateMock = jest.fn();
const metricsEventMock = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
encryptionPublicKeyManagerConstructorMock.mockReturnValue(
encryptionPublicKeyManagerMock,
);
encryptionPublicKeyController = new EncryptionPublicKeyController({
messenger: messengerMock as any,
keyringController: keyringControllerMock as any,
getState: getStateMock as any,
metricsEvent: metricsEventMock as any,
} as EncryptionPublicKeyControllerOptions);
});
describe('unapprovedMsgCount', () => {
it('returns value from message manager getter', () => {
encryptionPublicKeyManagerMock.getUnapprovedMessagesCount.mockReturnValueOnce(
10,
);
expect(encryptionPublicKeyController.unapprovedMsgCount).toBe(10);
});
});
describe('resetState', () => {
it('sets state to initial state', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
encryptionPublicKeyController.update(() => ({
unapprovedEncryptionPublicKeyMsgs: {
[messageIdMock]: messageMock,
} as any,
unapprovedEncryptionPublicKeyMsgCount: 1,
}));
encryptionPublicKeyController.resetState();
expect(encryptionPublicKeyController.state).toEqual({
unapprovedEncryptionPublicKeyMsgs: {},
unapprovedEncryptionPublicKeyMsgCount: 0,
});
});
});
describe('rejectUnapproved', () => {
beforeEach(() => {
const messages = {
[messageIdMock]: messageMock,
[messageIdMock2]: messageMock,
};
encryptionPublicKeyManagerMock.getUnapprovedMessages.mockReturnValueOnce(
messages as any,
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
encryptionPublicKeyController.update(() => ({
unapprovedEncryptionPublicKeyMsgs: messages as any,
}));
});
it('rejects all messages in the message manager', () => {
encryptionPublicKeyController.rejectUnapproved('Test Reason');
expect(
encryptionPublicKeyManagerMock.rejectMessage,
).toHaveBeenCalledTimes(2);
expect(encryptionPublicKeyManagerMock.rejectMessage).toHaveBeenCalledWith(
messageIdMock,
);
expect(encryptionPublicKeyManagerMock.rejectMessage).toHaveBeenCalledWith(
messageIdMock2,
);
});
it('fires metrics event with reject reason', () => {
encryptionPublicKeyController.rejectUnapproved('Test Reason');
expect(metricsEventMock).toHaveBeenCalledTimes(2);
expect(metricsEventMock).toHaveBeenLastCalledWith({
event: 'Test Reason',
category: MetaMetricsEventCategory.Messages,
properties: {
action: 'Encryption public key Request',
},
});
});
});
describe('clearUnapproved', () => {
it('resets state in all message managers', () => {
encryptionPublicKeyController.clearUnapproved();
const defaultState = {
unapprovedMessages: {},
unapprovedMessagesCount: 0,
};
expect(encryptionPublicKeyManagerMock.update).toHaveBeenCalledTimes(1);
expect(encryptionPublicKeyManagerMock.update).toHaveBeenCalledWith(
defaultState,
);
});
});
describe('newRequestEncryptionPublicKey', () => {
it.each([
['Ledger', KeyringType.ledger],
['Trezor', KeyringType.trezor],
['Lattice', KeyringType.lattice],
['QR hardware', KeyringType.qr],
])(
'throws if keyring is not supported',
async (keyringName, keyringType) => {
keyringControllerMock.getKeyringForAccount.mockResolvedValueOnce({
type: keyringType,
});
await expect(
encryptionPublicKeyController.newRequestEncryptionPublicKey(
addressMock,
requestMock,
),
).rejects.toThrowError(
`${keyringName} does not support eth_getEncryptionPublicKey.`,
);
},
);
it('adds message to message manager', async () => {
keyringControllerMock.getKeyringForAccount.mockResolvedValueOnce(
keyringMock,
);
await encryptionPublicKeyController.newRequestEncryptionPublicKey(
addressMock,
requestMock,
);
expect(
encryptionPublicKeyManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledTimes(1);
expect(
encryptionPublicKeyManagerMock.addUnapprovedMessageAsync,
).toHaveBeenCalledWith({ from: addressMock }, requestMock);
});
});
describe('encryptionPublicKey', () => {
beforeEach(() => {
encryptionPublicKeyManagerMock.approveMessage.mockResolvedValueOnce({
from: messageParamsMock.data,
});
keyringControllerMock.getEncryptionPublicKey.mockResolvedValueOnce(
publicKeyMock,
);
});
it('approves message and signs', async () => {
await encryptionPublicKeyController.encryptionPublicKey(
messageParamsMock,
);
expect(
keyringControllerMock.getEncryptionPublicKey,
).toHaveBeenCalledTimes(1);
expect(keyringControllerMock.getEncryptionPublicKey).toHaveBeenCalledWith(
messageParamsMock.data,
);
expect(
encryptionPublicKeyManagerMock.setMessageStatusAndResult,
).toHaveBeenCalledTimes(1);
expect(
encryptionPublicKeyManagerMock.setMessageStatusAndResult,
).toHaveBeenCalledWith(
messageParamsMock.metamaskId,
publicKeyMock,
'received',
);
});
it('returns current state', async () => {
getStateMock.mockReturnValueOnce(stateMock);
expect(
await encryptionPublicKeyController.encryptionPublicKey(
messageParamsMock,
),
).toEqual(stateMock);
});
it('accepts approval', async () => {
await encryptionPublicKeyController.encryptionPublicKey(
messageParamsMock,
);
expect(messengerMock.call).toHaveBeenCalledTimes(1);
expect(messengerMock.call).toHaveBeenCalledWith(
'ApprovalController:acceptRequest',
messageParamsMock.metamaskId,
);
});
it('rejects message on error', async () => {
keyringControllerMock.getEncryptionPublicKey.mockReset();
keyringControllerMock.getEncryptionPublicKey.mockRejectedValue(
new Error('Test Error'),
);
await expect(
encryptionPublicKeyController.encryptionPublicKey(messageParamsMock),
).rejects.toThrow('Test Error');
expect(
encryptionPublicKeyManagerMock.rejectMessage,
).toHaveBeenCalledTimes(1);
expect(encryptionPublicKeyManagerMock.rejectMessage).toHaveBeenCalledWith(
messageParamsMock.metamaskId,
);
});
it('rejects approval on error', async () => {
keyringControllerMock.getEncryptionPublicKey.mockReset();
keyringControllerMock.getEncryptionPublicKey.mockRejectedValue(
new Error('Test Error'),
);
await expect(
encryptionPublicKeyController.encryptionPublicKey(messageParamsMock),
).rejects.toThrow('Test Error');
expect(messengerMock.call).toHaveBeenCalledTimes(1);
expect(messengerMock.call).toHaveBeenCalledWith(
'ApprovalController:rejectRequest',
messageParamsMock.metamaskId,
'Cancel',
);
});
});
describe('cancelEncryptionPublicKey', () => {
it('rejects message using message manager', async () => {
encryptionPublicKeyController.cancelEncryptionPublicKey(messageIdMock);
expect(
encryptionPublicKeyManagerMock.rejectMessage,
).toHaveBeenCalledTimes(1);
expect(encryptionPublicKeyManagerMock.rejectMessage).toHaveBeenCalledWith(
messageParamsMock.metamaskId,
);
});
it('rejects approval using approval controller', async () => {
encryptionPublicKeyController.cancelEncryptionPublicKey(messageIdMock);
expect(messengerMock.call).toHaveBeenCalledTimes(1);
expect(messengerMock.call).toHaveBeenCalledWith(
'ApprovalController:rejectRequest',
messageParamsMock.metamaskId,
'Cancel',
);
});
});
describe('message manager events', () => {
it('bubbles update badge event from EncryptionPublicKeyManager', () => {
const mockListener = jest.fn();
encryptionPublicKeyController.hub.on('updateBadge', mockListener);
(encryptionPublicKeyManagerMock.hub.on as any).mock.calls[0][1]();
expect(mockListener).toHaveBeenCalledTimes(1);
});
it('requires approval on unapproved message event from EncryptionPublicKeyManager', () => {
messengerMock.call.mockResolvedValueOnce({});
(encryptionPublicKeyManagerMock.hub.on as any).mock.calls[1][1](
messageParamsMock,
);
expect(messengerMock.call).toHaveBeenCalledTimes(1);
expect(messengerMock.call).toHaveBeenCalledWith(
'ApprovalController:addRequest',
{
id: messageIdMock,
origin: messageParamsMock.origin,
type: 'eth_getEncryptionPublicKey',
},
true,
);
});
it('updates state on EncryptionPublicKeyManager state change', async () => {
await encryptionPublicKeyManagerMock.subscribe.mock.calls[0][0]({
unapprovedMessages: { [messageIdMock]: coreMessageMock as any },
unapprovedMessagesCount: 3,
});
expect(encryptionPublicKeyController.state).toEqual({
unapprovedEncryptionPublicKeyMsgs: {
[messageIdMock]: stateMessageMock as any,
},
unapprovedEncryptionPublicKeyMsgCount: 3,
});
});
});
});

View File

@ -0,0 +1,421 @@
import EventEmitter from 'events';
import log from 'loglevel';
import {
EncryptionPublicKeyManager,
EncryptionPublicKeyParamsMetamask,
} from '@metamask/message-manager';
import { KeyringController } from '@metamask/eth-keyring-controller';
import {
AbstractMessageManager,
AbstractMessage,
MessageManagerState,
AbstractMessageParams,
AbstractMessageParamsMetamask,
OriginalRequest,
} from '@metamask/message-manager/dist/AbstractMessageManager';
import {
BaseControllerV2,
RestrictedControllerMessenger,
} from '@metamask/base-controller';
import { Patch } from 'immer';
import {
AcceptRequest,
AddApprovalRequest,
RejectRequest,
} from '@metamask/approval-controller';
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
import { KeyringType } from '../../../shared/constants/keyring';
import { ORIGIN_METAMASK } from '../../../shared/constants/app';
const controllerName = 'EncryptionPublicKeyController';
const methodNameGetEncryptionPublicKey = 'eth_getEncryptionPublicKey';
const stateMetadata = {
unapprovedEncryptionPublicKeyMsgs: { persist: false, anonymous: false },
unapprovedEncryptionPublicKeyMsgCount: { persist: false, anonymous: false },
};
const getDefaultState = () => ({
unapprovedEncryptionPublicKeyMsgs: {},
unapprovedEncryptionPublicKeyMsgCount: 0,
});
export type CoreMessage = AbstractMessage & {
messageParams: AbstractMessageParams;
};
export type StateMessage = Required<
Omit<AbstractMessage, 'securityProviderResponse'>
> & {
msgParams: string;
};
export type EncryptionPublicKeyControllerState = {
unapprovedEncryptionPublicKeyMsgs: Record<string, StateMessage>;
unapprovedEncryptionPublicKeyMsgCount: number;
};
export type GetEncryptionPublicKeyState = {
type: `${typeof controllerName}:getState`;
handler: () => EncryptionPublicKeyControllerState;
};
export type EncryptionPublicKeyStateChange = {
type: `${typeof controllerName}:stateChange`;
payload: [EncryptionPublicKeyControllerState, Patch[]];
};
export type EncryptionPublicKeyControllerActions = GetEncryptionPublicKeyState;
export type EncryptionPublicKeyControllerEvents =
EncryptionPublicKeyStateChange;
type AllowedActions = AddApprovalRequest | AcceptRequest | RejectRequest;
export type EncryptionPublicKeyControllerMessenger =
RestrictedControllerMessenger<
typeof controllerName,
EncryptionPublicKeyControllerActions | AllowedActions,
EncryptionPublicKeyControllerEvents,
AllowedActions['type'],
never
>;
export type EncryptionPublicKeyControllerOptions = {
messenger: EncryptionPublicKeyControllerMessenger;
keyringController: KeyringController;
getState: () => any;
metricsEvent: (payload: any, options?: any) => void;
};
/**
* Controller for requesting encryption public key requests requiring user approval.
*/
export default class EncryptionPublicKeyController extends BaseControllerV2<
typeof controllerName,
EncryptionPublicKeyControllerState,
EncryptionPublicKeyControllerMessenger
> {
hub: EventEmitter;
private _keyringController: KeyringController;
private _getState: () => any;
private _encryptionPublicKeyManager: EncryptionPublicKeyManager;
private _metricsEvent: (payload: any, options?: any) => void;
/**
* Construct a EncryptionPublicKey controller.
*
* @param options - The controller options.
* @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller.
* @param options.keyringController - An instance of a keyring controller used to extract the encryption public key.
* @param options.getState - Callback to retrieve all user state.
* @param options.metricsEvent - A function for emitting a metric event.
*/
constructor({
messenger,
keyringController,
getState,
metricsEvent,
}: EncryptionPublicKeyControllerOptions) {
super({
name: controllerName,
metadata: stateMetadata,
messenger,
state: getDefaultState(),
});
this._keyringController = keyringController;
this._getState = getState;
this._metricsEvent = metricsEvent;
this.hub = new EventEmitter();
this._encryptionPublicKeyManager = new EncryptionPublicKeyManager(
undefined,
undefined,
undefined,
['received'],
);
this._encryptionPublicKeyManager.hub.on('updateBadge', () => {
this.hub.emit('updateBadge');
});
this._encryptionPublicKeyManager.hub.on(
'unapprovedMessage',
(msgParams: AbstractMessageParamsMetamask) => {
this._requestApproval(msgParams, methodNameGetEncryptionPublicKey);
},
);
this._subscribeToMessageState(
this._encryptionPublicKeyManager,
(state, newMessages, messageCount) => {
state.unapprovedEncryptionPublicKeyMsgs = newMessages;
state.unapprovedEncryptionPublicKeyMsgCount = messageCount;
},
);
}
/**
* A getter for the number of 'unapproved' Messages in this.messages
*
* @returns The number of 'unapproved' Messages in this.messages
*/
get unapprovedMsgCount(): number {
return this._encryptionPublicKeyManager.getUnapprovedMessagesCount();
}
/**
* Reset the controller state to the initial state.
*/
resetState() {
this.update(() => getDefaultState());
}
/**
* Called when a Dapp uses the eth_getEncryptionPublicKey method, to request user approval.
*
* @param address - The address from the encryption public key will be extracted.
* @param [req] - The original request, containing the origin.
*/
async newRequestEncryptionPublicKey(
address: string,
req: OriginalRequest,
): Promise<string> {
const keyring = await this._keyringController.getKeyringForAccount(address);
switch (keyring.type) {
case KeyringType.ledger: {
return new Promise((_, reject) => {
reject(
new Error('Ledger does not support eth_getEncryptionPublicKey.'),
);
});
}
case KeyringType.trezor: {
return new Promise((_, reject) => {
reject(
new Error('Trezor does not support eth_getEncryptionPublicKey.'),
);
});
}
case KeyringType.lattice: {
return new Promise((_, reject) => {
reject(
new Error('Lattice does not support eth_getEncryptionPublicKey.'),
);
});
}
case KeyringType.qr: {
return Promise.reject(
new Error('QR hardware does not support eth_getEncryptionPublicKey.'),
);
}
default: {
return this._encryptionPublicKeyManager.addUnapprovedMessageAsync(
{ from: address },
req,
);
}
}
}
/**
* Signifies a user's approval to receiving encryption public key in queue.
*
* @param msgParams - The params of the message to receive & return to the Dapp.
* @returns A full state update.
*/
async encryptionPublicKey(msgParams: EncryptionPublicKeyParamsMetamask) {
log.info('MetaMaskController - encryptionPublicKey');
const messageId = msgParams.metamaskId as string;
// sets the status op the message to 'approved'
// and removes the metamaskId for decryption
try {
const cleanMessageParams =
await this._encryptionPublicKeyManager.approveMessage(msgParams);
// EncryptionPublicKey message
const publicKey = await this._keyringController.getEncryptionPublicKey(
cleanMessageParams.from,
);
// tells the listener that the message has been processed
// and can be returned to the dapp
this._encryptionPublicKeyManager.setMessageStatusAndResult(
messageId,
publicKey,
'received',
);
this._acceptApproval(messageId);
return this._getState();
} catch (error) {
log.info(
'MetaMaskController - eth_getEncryptionPublicKey failed.',
error,
);
this._cancelAbstractMessage(this._encryptionPublicKeyManager, messageId);
throw error;
}
}
/**
* Used to cancel a message submitted via eth_getEncryptionPublicKey.
*
* @param msgId - The id of the message to cancel.
*/
cancelEncryptionPublicKey(msgId: string) {
this._cancelAbstractMessage(this._encryptionPublicKeyManager, msgId);
}
/**
* Reject all unapproved messages of any type.
*
* @param reason - A message to indicate why.
*/
rejectUnapproved(reason?: string) {
Object.keys(
this._encryptionPublicKeyManager.getUnapprovedMessages(),
).forEach((messageId) => {
this._cancelAbstractMessage(
this._encryptionPublicKeyManager,
messageId,
reason,
);
});
}
/**
* Clears all unapproved messages from memory.
*/
clearUnapproved() {
this._encryptionPublicKeyManager.update({
unapprovedMessages: {},
unapprovedMessagesCount: 0,
});
}
private _cancelAbstractMessage(
messageManager: AbstractMessageManager<
AbstractMessage,
AbstractMessageParams,
AbstractMessageParamsMetamask
>,
messageId: string,
reason?: string,
) {
if (reason) {
this._metricsEvent({
event: reason,
category: MetaMetricsEventCategory.Messages,
properties: {
action: 'Encryption public key Request',
},
});
}
messageManager.rejectMessage(messageId);
this._rejectApproval(messageId);
return this._getState();
}
private _subscribeToMessageState(
messageManager: AbstractMessageManager<
AbstractMessage,
AbstractMessageParams,
AbstractMessageParamsMetamask
>,
updateState: (
state: EncryptionPublicKeyControllerState,
newMessages: Record<string, StateMessage>,
messageCount: number,
) => void,
) {
messageManager.subscribe(
async (state: MessageManagerState<AbstractMessage>) => {
const newMessages = await this._migrateMessages(
state.unapprovedMessages as any,
);
this.update((draftState) => {
updateState(draftState, newMessages, state.unapprovedMessagesCount);
});
},
);
}
private async _migrateMessages(
coreMessages: Record<string, CoreMessage>,
): Promise<Record<string, StateMessage>> {
const stateMessages: Record<string, StateMessage> = {};
for (const messageId of Object.keys(coreMessages)) {
const coreMessage = coreMessages[messageId];
const stateMessage = await this._migrateMessage(coreMessage);
stateMessages[messageId] = stateMessage;
}
return stateMessages;
}
private async _migrateMessage(
coreMessage: CoreMessage,
): Promise<StateMessage> {
const { messageParams, ...coreMessageData } = coreMessage;
// Core message managers use messageParams but frontend uses msgParams with lots of references
const stateMessage = {
...coreMessageData,
rawSig: coreMessage.rawSig as string,
msgParams: messageParams.from,
origin: messageParams.origin,
};
return stateMessage;
}
private _requestApproval(
msgParams: AbstractMessageParamsMetamask,
type: string,
) {
const id = msgParams.metamaskId as string;
const origin = msgParams.origin || ORIGIN_METAMASK;
this.messagingSystem
.call(
'ApprovalController:addRequest',
{
id,
origin,
type,
},
true,
)
.catch(() => {
// Intentionally ignored as promise not currently used
});
}
private _acceptApproval(messageId: string) {
this.messagingSystem.call('ApprovalController:acceptRequest', messageId);
}
private _rejectApproval(messageId: string) {
this.messagingSystem.call(
'ApprovalController:rejectRequest',
messageId,
'Cancel',
);
}
}

View File

@ -1 +0,0 @@
export { default, NetworkControllerEventTypes } from './network-controller';

View File

@ -0,0 +1 @@
export * from './network-controller';

View File

@ -1,679 +0,0 @@
import { strict as assert } from 'assert';
import EventEmitter from 'events';
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import {
createSwappableProxy,
createEventEmitterProxy,
} from '@metamask/swappable-obj-proxy';
import EthQuery from 'eth-query';
// ControllerMessenger is referred to in the JSDocs
// eslint-disable-next-line no-unused-vars
import { ControllerMessenger } from '@metamask/base-controller';
import { v4 as random } from 'uuid';
import { hasProperty, isPlainObject } from '@metamask/utils';
import { errorCodes } from 'eth-rpc-errors';
import {
INFURA_PROVIDER_TYPES,
BUILT_IN_NETWORKS,
INFURA_BLOCKED_KEY,
TEST_NETWORK_TICKER_MAP,
CHAIN_IDS,
NETWORK_TYPES,
NetworkStatus,
} from '../../../../shared/constants/network';
import {
isPrefixedFormattedHexString,
isSafeChainId,
} from '../../../../shared/modules/network.utils';
import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics';
import { createNetworkClient } from './create-network-client';
/**
* @typedef {object} NetworkConfiguration
* @property {string} rpcUrl - RPC target URL.
* @property {string} chainId - Network ID as per EIP-155
* @property {string} ticker - Currency ticker.
* @property {object} [rpcPrefs] - Personalized preferences.
* @property {string} [nickname] - Personalized network name.
*/
function buildDefaultProviderConfigState() {
if (process.env.IN_TEST) {
return {
type: NETWORK_TYPES.RPC,
rpcUrl: 'http://localhost:8545',
chainId: '0x539',
nickname: 'Localhost 8545',
ticker: 'ETH',
};
} else if (
process.env.METAMASK_DEBUG ||
process.env.METAMASK_ENV === 'test'
) {
return {
type: NETWORK_TYPES.GOERLI,
chainId: CHAIN_IDS.GOERLI,
ticker: TEST_NETWORK_TICKER_MAP.GOERLI,
};
}
return {
type: NETWORK_TYPES.MAINNET,
chainId: CHAIN_IDS.MAINNET,
ticker: 'ETH',
};
}
function buildDefaultNetworkIdState() {
return null;
}
function buildDefaultNetworkStatusState() {
return NetworkStatus.Unknown;
}
function buildDefaultNetworkDetailsState() {
return {
EIPS: {
1559: undefined,
},
};
}
function buildDefaultNetworkConfigurationsState() {
return {};
}
/**
* The name of the controller.
*/
const name = 'NetworkController';
/**
* The set of event types that this controller can publish via its messenger.
*/
export const NetworkControllerEventTypes = {
/**
* Fired after the current network is changed.
*/
NetworkDidChange: `${name}:networkDidChange`,
/**
* Fired when there is a request to change the current network, but no state
* changes have occurred yet.
*/
NetworkWillChange: `${name}:networkWillChange`,
/**
* Fired after the network is changed to an Infura network, but when Infura
* returns an error denying support for the user's location.
*/
InfuraIsBlocked: `${name}:infuraIsBlocked`,
/**
* Fired after the network is changed to an Infura network and Infura does not
* return an error denying support for the user's location, or after the
* network is changed to a custom network.
*/
InfuraIsUnblocked: `${name}:infuraIsUnblocked`,
};
export default class NetworkController extends EventEmitter {
/**
* Construct a NetworkController.
*
* @param {object} options - Options for this controller.
* @param {ControllerMessenger} options.messenger - The controller messenger.
* @param {object} [options.state] - Initial controller state.
* @param {string} [options.infuraProjectId] - The Infura project ID.
* @param {string} [options.trackMetaMetricsEvent] - A method to forward events to the MetaMetricsController
*/
constructor({
messenger,
state = {},
infuraProjectId,
trackMetaMetricsEvent,
} = {}) {
super();
this.messenger = messenger;
// create stores
this.providerStore = new ObservableStore(
state.provider || buildDefaultProviderConfigState(),
);
this.previousProviderStore = new ObservableStore(
this.providerStore.getState(),
);
this.networkIdStore = new ObservableStore(buildDefaultNetworkIdState());
this.networkStatusStore = new ObservableStore(
buildDefaultNetworkStatusState(),
);
// We need to keep track of a few details about the current network.
// Ideally we'd merge this.networkStatusStore with this new store, but doing
// so will require a decent sized refactor of how we're accessing network
// state. Currently this is only used for detecting EIP-1559 support but can
// be extended to track other network details.
this.networkDetails = new ObservableStore(
state.networkDetails || buildDefaultNetworkDetailsState(),
);
this.networkConfigurationsStore = new ObservableStore(
state.networkConfigurations || buildDefaultNetworkConfigurationsState(),
);
this.store = new ComposedStore({
provider: this.providerStore,
previousProviderStore: this.previousProviderStore,
networkId: this.networkIdStore,
networkStatus: this.networkStatusStore,
networkDetails: this.networkDetails,
networkConfigurations: this.networkConfigurationsStore,
});
// provider and block tracker
this._provider = null;
this._blockTracker = null;
// provider and block tracker proxies - because the network changes
this._providerProxy = null;
this._blockTrackerProxy = null;
if (!infuraProjectId || typeof infuraProjectId !== 'string') {
throw new Error('Invalid Infura project ID');
}
this._infuraProjectId = infuraProjectId;
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
}
/**
* Destroy the network controller, stopping any ongoing polling.
*
* In-progress requests will not be aborted.
*/
async destroy() {
await this._blockTracker?.destroy();
}
async initializeProvider() {
const { type, rpcUrl, chainId } = this.providerStore.getState();
this._configureProvider({ type, rpcUrl, chainId });
await this.lookupNetwork();
}
// return the proxies so the references will always be good
getProviderAndBlockTracker() {
const provider = this._providerProxy;
const blockTracker = this._blockTrackerProxy;
return { provider, blockTracker };
}
/**
* Determines whether the network supports EIP-1559 by checking whether the
* latest block has a `baseFeePerGas` property, then updates state
* appropriately.
*
* @returns {Promise<boolean>} A promise that resolves to true if the network
* supports EIP-1559 and false otherwise.
*/
async getEIP1559Compatibility() {
const { EIPS } = this.networkDetails.getState();
// NOTE: This isn't necessary anymore because the block cache middleware
// already prevents duplicate requests from taking place
if (EIPS[1559] !== undefined) {
return EIPS[1559];
}
const supportsEIP1559 = await this._determineEIP1559Compatibility();
this.networkDetails.updateState({
EIPS: {
...this.networkDetails.getState().EIPS,
1559: supportsEIP1559,
},
});
return supportsEIP1559;
}
/**
* Captures information about the currently selected network namely,
* the network ID and whether the network supports EIP-1559 and then uses
* the results of these requests to determine the status of the network.
*/
async lookupNetwork() {
const { chainId, type } = this.providerStore.getState();
let networkChanged = false;
let networkId;
let supportsEIP1559;
let networkStatus;
if (!this._provider) {
log.warn(
'NetworkController - lookupNetwork aborted due to missing provider',
);
return;
}
if (!chainId) {
log.warn(
'NetworkController - lookupNetwork aborted due to missing chainId',
);
this._resetNetworkId();
this._resetNetworkStatus();
this._resetNetworkDetails();
return;
}
const isInfura = INFURA_PROVIDER_TYPES.includes(type);
const listener = () => {
networkChanged = true;
this.messenger.unsubscribe(
NetworkControllerEventTypes.NetworkDidChange,
listener,
);
};
this.messenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
listener,
);
try {
const results = await Promise.all([
this._getNetworkId(),
this._determineEIP1559Compatibility(),
]);
networkId = results[0];
supportsEIP1559 = results[1];
networkStatus = NetworkStatus.Available;
} catch (error) {
if (hasProperty(error, 'code')) {
let responseBody;
try {
responseBody = JSON.parse(error.message);
} catch {
// error.message must not be JSON
}
if (
isPlainObject(responseBody) &&
responseBody.error === INFURA_BLOCKED_KEY
) {
networkStatus = NetworkStatus.Blocked;
} else if (error.code === errorCodes.rpc.internal) {
networkStatus = NetworkStatus.Unknown;
} else {
networkStatus = NetworkStatus.Unavailable;
}
} else {
log.warn(
'NetworkController - could not determine network status',
error,
);
networkStatus = NetworkStatus.Unknown;
}
}
if (networkChanged) {
// If the network has changed, then `lookupNetwork` either has been or is
// in the process of being called, so we don't need to go further.
return;
}
this.messenger.unsubscribe(
NetworkControllerEventTypes.NetworkDidChange,
listener,
);
this.networkStatusStore.putState(networkStatus);
if (networkStatus === NetworkStatus.Available) {
this.networkIdStore.putState(networkId);
this.networkDetails.updateState({
EIPS: {
...this.networkDetails.getState().EIPS,
1559: supportsEIP1559,
},
});
} else {
this._resetNetworkId();
this._resetNetworkDetails();
}
if (isInfura) {
if (networkStatus === NetworkStatus.Available) {
this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked);
} else if (networkStatus === NetworkStatus.Blocked) {
this.messenger.publish(NetworkControllerEventTypes.InfuraIsBlocked);
}
} else {
// Always publish infuraIsUnblocked regardless of network status to
// prevent consumers from being stuck in a blocked state if they were
// previously connected to an Infura network that was blocked
this.messenger.publish(NetworkControllerEventTypes.InfuraIsUnblocked);
}
}
/**
* A method for setting the currently selected network provider by networkConfigurationId.
*
* @param {string} networkConfigurationId - the universal unique identifier that corresponds to the network configuration to set as active.
* @returns {string} The rpcUrl of the network that was just set as active
*/
setActiveNetwork(networkConfigurationId) {
const targetNetwork =
this.networkConfigurationsStore.getState()[networkConfigurationId];
if (!targetNetwork) {
throw new Error(
`networkConfigurationId ${networkConfigurationId} does not match a configured networkConfiguration`,
);
}
this._setProviderConfig({
type: NETWORK_TYPES.RPC,
...targetNetwork,
});
return targetNetwork.rpcUrl;
}
setProviderType(type) {
assert.notStrictEqual(
type,
NETWORK_TYPES.RPC,
`NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPES.RPC}". Use "setActiveNetwork"`,
);
assert.ok(
INFURA_PROVIDER_TYPES.includes(type),
`Unknown Infura provider type "${type}".`,
);
const { chainId, ticker, blockExplorerUrl } = BUILT_IN_NETWORKS[type];
this._setProviderConfig({
type,
rpcUrl: '',
chainId,
ticker: ticker ?? 'ETH',
nickname: '',
rpcPrefs: { blockExplorerUrl },
});
}
resetConnection() {
this._setProviderConfig(this.providerStore.getState());
}
rollbackToPreviousProvider() {
const config = this.previousProviderStore.getState();
this.providerStore.putState(config);
this._switchNetwork(config);
}
//
// Private
//
/**
* Method to return the latest block for the current network
*
* @returns {object} Block header
*/
_getLatestBlock() {
const { provider } = this.getProviderAndBlockTracker();
const ethQuery = new EthQuery(provider);
return new Promise((resolve, reject) => {
ethQuery.sendAsync(
{ method: 'eth_getBlockByNumber', params: ['latest', false] },
(error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
},
);
});
}
/**
* Get the network ID for the current selected network
*
* @returns {string} The network ID for the current network.
*/
async _getNetworkId() {
const { provider } = this.getProviderAndBlockTracker();
const ethQuery = new EthQuery(provider);
return await new Promise((resolve, reject) => {
ethQuery.sendAsync({ method: 'net_version' }, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
/**
* Clears the stored network ID.
*/
_resetNetworkId() {
this.networkIdStore.putState(buildDefaultNetworkIdState());
}
/**
* Resets network status to the default ("unknown").
*/
_resetNetworkStatus() {
this.networkStatusStore.putState(buildDefaultNetworkStatusState());
}
/**
* Clears details previously stored for the network.
*/
_resetNetworkDetails() {
this.networkDetails.putState(buildDefaultNetworkDetailsState());
}
/**
* Sets the provider config and switches the network.
*
* @param config
*/
_setProviderConfig(config) {
this.previousProviderStore.putState(this.providerStore.getState());
this.providerStore.putState(config);
this._switchNetwork(config);
}
/**
* Retrieves the latest block from the currently selected network; if the
* block has a `baseFeePerGas` property, then we know that the network
* supports EIP-1559; otherwise it doesn't.
*
* @returns {Promise<boolean>} A promise that resolves to true if the network
* supports EIP-1559 and false otherwise.
*/
async _determineEIP1559Compatibility() {
const latestBlock = await this._getLatestBlock();
return latestBlock && latestBlock.baseFeePerGas !== undefined;
}
_switchNetwork(opts) {
this.messenger.publish(NetworkControllerEventTypes.NetworkWillChange);
this._resetNetworkId();
this._resetNetworkStatus();
this._resetNetworkDetails();
this._configureProvider(opts);
this.messenger.publish(NetworkControllerEventTypes.NetworkDidChange);
this.lookupNetwork();
}
_configureProvider({ type, rpcUrl, chainId }) {
// infura type-based endpoints
const isInfura = INFURA_PROVIDER_TYPES.includes(type);
if (isInfura) {
this._configureInfuraProvider({
type,
infuraProjectId: this._infuraProjectId,
});
// url-based rpc endpoints
} else if (type === NETWORK_TYPES.RPC) {
this._configureStandardProvider(rpcUrl, chainId);
} else {
throw new Error(
`NetworkController - _configureProvider - unknown type "${type}"`,
);
}
}
_configureInfuraProvider({ type, infuraProjectId }) {
log.info('NetworkController - configureInfuraProvider', type);
const { provider, blockTracker } = createNetworkClient({
network: type,
infuraProjectId,
type: 'infura',
});
this._setProviderAndBlockTracker({ provider, blockTracker });
}
_configureStandardProvider(rpcUrl, chainId) {
log.info('NetworkController - configureStandardProvider', rpcUrl);
const { provider, blockTracker } = createNetworkClient({
chainId,
rpcUrl,
type: 'custom',
});
this._setProviderAndBlockTracker({ provider, blockTracker });
}
_setProviderAndBlockTracker({ provider, blockTracker }) {
// update or initialize proxies
if (this._providerProxy) {
this._providerProxy.setTarget(provider);
} else {
this._providerProxy = createSwappableProxy(provider);
}
if (this._blockTrackerProxy) {
this._blockTrackerProxy.setTarget(blockTracker);
} else {
this._blockTrackerProxy = createEventEmitterProxy(blockTracker, {
eventFilter: 'skipInternal',
});
}
// set new provider and blockTracker
this._provider = provider;
this._blockTracker = blockTracker;
}
/**
* Network Configuration management functions
*/
/**
* Adds a network configuration if the rpcUrl is not already present on an
* existing network configuration. Otherwise updates the entry with the matching rpcUrl.
*
* @param {NetworkConfiguration} networkConfiguration - The network configuration to add or, if rpcUrl matches an existing entry, to modify.
* @param {object} options
* @param {boolean} options.setActive - An option to set the newly added networkConfiguration as the active provider.
* @param {string} options.referrer - The site from which the call originated, or 'metamask' for internal calls - used for event metrics.
* @param {string} options.source - Where the upsertNetwork event originated (i.e. from a dapp or from the network form)- used for event metrics.
* @returns {string} id for the added or updated network configuration
*/
upsertNetworkConfiguration(
{ rpcUrl, chainId, ticker, nickname, rpcPrefs },
{ setActive = false, referrer, source },
) {
assert.ok(
isPrefixedFormattedHexString(chainId),
`Invalid chain ID "${chainId}": invalid hex string.`,
);
assert.ok(
isSafeChainId(parseInt(chainId, 16)),
`Invalid chain ID "${chainId}": numerical value greater than max safe value.`,
);
if (!rpcUrl) {
throw new Error(
'An rpcUrl is required to add or update network configuration',
);
}
if (!referrer || !source) {
throw new Error(
'referrer and source are required arguments for adding or updating a network configuration',
);
}
try {
// eslint-disable-next-line no-new
new URL(rpcUrl);
} catch (e) {
if (e.message.includes('Invalid URL')) {
throw new Error('rpcUrl must be a valid URL');
}
}
if (!ticker) {
throw new Error(
'A ticker is required to add or update networkConfiguration',
);
}
const networkConfigurations = this.networkConfigurationsStore.getState();
const newNetworkConfiguration = {
rpcUrl,
chainId,
ticker,
nickname,
rpcPrefs,
};
const oldNetworkConfigurationId = Object.values(networkConfigurations).find(
(networkConfiguration) =>
networkConfiguration.rpcUrl?.toLowerCase() === rpcUrl?.toLowerCase(),
)?.id;
const newNetworkConfigurationId = oldNetworkConfigurationId || random();
this.networkConfigurationsStore.putState({
...networkConfigurations,
[newNetworkConfigurationId]: {
...newNetworkConfiguration,
id: newNetworkConfigurationId,
},
});
if (!oldNetworkConfigurationId) {
this._trackMetaMetricsEvent({
event: 'Custom Network Added',
category: MetaMetricsEventCategory.Network,
referrer: {
url: referrer,
},
properties: {
chain_id: chainId,
symbol: ticker,
source,
},
});
}
if (setActive) {
this.setActiveNetwork(newNetworkConfigurationId);
}
return newNetworkConfigurationId;
}
/**
* Removes network configuration from state.
*
* @param {string} networkConfigurationId - the unique id for the network configuration to remove.
*/
removeNetworkConfiguration(networkConfigurationId) {
const networkConfigurations = {
...this.networkConfigurationsStore.getState(),
};
delete networkConfigurations[networkConfigurationId];
this.networkConfigurationsStore.putState(networkConfigurations);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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(

View File

@ -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 = {

View File

@ -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`.
*/
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;
};
/**
* 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 },
});
}
type MockRpcCallOptions = {
// A nock scope (a set of mocked requests scoped to a certain base URL).
nockScope: nock.Scope;
} & CurriedMockRpcCallOptions;
/**
* 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 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<any>} 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<MockBlockTrackerRequestOptions, 'nockScope'>) => void, mockAllBlockTrackerRequests: (options: Omit<MockBlockTrackerRequestOptions, 'nockScope'>) => void, mockRpcCall: (options: Omit<MockRpcCallOptions, 'nockScope'>) => 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<any>} 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<any>} 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<void>,
) {
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<JsonRpcRequest>) => Promise<any>, makeRpcCallsInSeries: (requests: Partial<JsonRpcRequest>[]) => Promise<any>}} 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<any>;
makeRpcCallsInSeries: (requests: Request[]) => Promise<any[]>;
};
/**
* 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<any>} 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<any>} 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<any>,
) {
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;

View File

@ -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(

View File

@ -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(

View File

@ -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: <https://ethereum.github.io/execution-apis/api-documentation/>
// Infura documentation: <https://docs.infura.io/infura/networks/ethereum/json-rpc-methods>

View File

@ -4,7 +4,7 @@ import { ControllerMessenger } from '@metamask/base-controller';
import { TokenListController } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '../../../shared/constants/network';
import PreferencesController from './preferences';
import NetworkController from './network';
import { NetworkController } from './network';
describe('preferences controller', function () {
let preferencesController;

View File

@ -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', () => {

View File

@ -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 },
@ -104,7 +105,6 @@ export type SignControllerOptions = {
messenger: SignControllerMessenger;
keyringController: KeyringController;
preferencesController: PreferencesController;
sendUpdate: () => void;
getState: () => any;
metricsEvent: (payload: any, options?: any) => void;
securityProviderRequest: (
@ -637,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);
}
}
}

View File

@ -52,7 +52,10 @@ import {
determineTransactionType,
isEIP1559Transaction,
} from '../../../../shared/modules/transaction.utils';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
import {
ORIGIN_METAMASK,
MESSAGE_TYPE,
} from '../../../../shared/constants/app';
import {
calcGasTotal,
getSwapsTokensReceivedFromTxMeta,
@ -156,6 +159,7 @@ export default class TransactionController extends EventEmitter {
this.getAccountType = opts.getAccountType;
this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails;
this.securityProviderRequest = opts.securityProviderRequest;
this.messagingSystem = opts.messenger;
this.memStore = new ObservableStore({});
@ -798,6 +802,7 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.getTransactionWithActionId(actionId);
if (existingTxMeta) {
this.emit('newUnapprovedTx', existingTxMeta);
this._requestApproval(existingTxMeta);
existingTxMeta = await this.addTransactionGasDefaults(existingTxMeta);
return existingTxMeta;
}
@ -870,6 +875,7 @@ export default class TransactionController extends EventEmitter {
this.addTransaction(txMeta);
this.emit('newUnapprovedTx', txMeta);
this._requestApproval(txMeta);
txMeta = await this.addTransactionGasDefaults(txMeta);
@ -1355,6 +1361,7 @@ export default class TransactionController extends EventEmitter {
try {
// approve
this.txStateManager.setTxStatusApproved(txId);
this._acceptApproval(txMeta);
// get next nonce
const fromAddress = txMeta.txParams.from;
// wait for a nonce
@ -1734,6 +1741,7 @@ export default class TransactionController extends EventEmitter {
async cancelTransaction(txId, actionId) {
const txMeta = this.txStateManager.getTransaction(txId);
this.txStateManager.setTxStatusRejected(txId);
this._rejectApproval(txMeta);
this._trackTransactionMetricsEvent(
txMeta,
TransactionMetaMetricsEvent.rejected,
@ -2596,4 +2604,54 @@ export default class TransactionController extends EventEmitter {
},
);
}
_requestApproval(txMeta) {
const id = this._getApprovalId(txMeta);
const { origin } = txMeta;
const type = MESSAGE_TYPE.TRANSACTION;
const requestData = { txId: txMeta.id };
this.messagingSystem
.call(
'ApprovalController:addRequest',
{
id,
origin,
type,
requestData,
},
true,
)
.catch(() => {
// Intentionally ignored as promise not currently used
});
}
_acceptApproval(txMeta) {
const id = this._getApprovalId(txMeta);
try {
this.messagingSystem.call('ApprovalController:acceptRequest', id);
} catch (error) {
log.error('Failed to accept transaction approval request', error);
}
}
_rejectApproval(txMeta) {
const id = this._getApprovalId(txMeta);
try {
this.messagingSystem.call(
'ApprovalController:rejectRequest',
id,
new Error('Rejected'),
);
} catch (error) {
log.error('Failed to reject transaction approval request', error);
}
}
_getApprovalId(txMeta) {
return String(txMeta.id);
}
}

View File

@ -29,7 +29,10 @@ import {
GasRecommendations,
} from '../../../../shared/constants/gas';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
import {
MESSAGE_TYPE,
ORIGIN_METAMASK,
} from '../../../../shared/constants/app';
import { NetworkStatus } from '../../../../shared/constants/network';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
import TransactionController from '.';
@ -52,7 +55,8 @@ describe('Transaction Controller', function () {
fromAccount,
fragmentExists,
networkStatusStore,
getCurrentChainId;
getCurrentChainId,
messengerMock;
beforeEach(function () {
fragmentExists = false;
@ -76,6 +80,7 @@ describe('Transaction Controller', function () {
blockTrackerStub.getLatestBlock = noop;
getCurrentChainId = sinon.stub().callsFake(() => currentChainId);
messengerMock = { call: sinon.stub().returns(Promise.resolve()) };
txController = new TransactionController({
provider,
@ -108,6 +113,7 @@ describe('Transaction Controller', function () {
getAccountType: () => 'MetaMask',
getDeviceModel: () => 'N/A',
securityProviderRequest: () => undefined,
messenger: messengerMock,
});
txController.nonceTracker.getNonceLock = () =>
Promise.resolve({ nextNonce: 0, releaseLock: noop });
@ -489,6 +495,67 @@ describe('Transaction Controller', function () {
{ message: 'MetaMask is having trouble connecting to the network' },
);
});
it('should create an approval request', async function () {
const txMeta = await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
ORIGIN_METAMASK,
);
assert.equal(messengerMock.call.callCount, 1);
assert.deepEqual(messengerMock.call.getCall(0).args, [
'ApprovalController:addRequest',
{
id: String(txMeta.id),
origin: ORIGIN_METAMASK,
requestData: { txId: txMeta.id },
type: MESSAGE_TYPE.TRANSACTION,
},
true, // Show popup
]);
});
it('should still create an approval request when called twice with same actionId', async function () {
await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
ORIGIN_METAMASK,
undefined,
undefined,
'12345',
);
const secondTxMeta = await txController.addUnapprovedTransaction(
undefined,
{
from: selectedAddress,
to: recipientAddress,
},
undefined,
undefined,
undefined,
'12345',
);
assert.equal(messengerMock.call.callCount, 2);
assert.deepEqual(messengerMock.call.getCall(1).args, [
'ApprovalController:addRequest',
{
id: String(secondTxMeta.id),
origin: ORIGIN_METAMASK,
requestData: { txId: secondTxMeta.id },
type: MESSAGE_TYPE.TRANSACTION,
},
true, // Show popup
]);
});
});
describe('#createCancelTransaction', function () {
@ -997,9 +1064,11 @@ describe('Transaction Controller', function () {
});
describe('#approveTransaction', function () {
it('does not overwrite set values', async function () {
const originalValue = '0x01';
const txMeta = {
let originalValue, txMeta, signStub, pubStub;
beforeEach(function () {
originalValue = '0x01';
txMeta = {
id: '1',
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
@ -1019,17 +1088,22 @@ describe('Transaction Controller', function () {
providerResultStub.eth_gasPrice = wrongValue;
providerResultStub.eth_estimateGas = '0x5209';
const signStub = sinon
signStub = sinon
.stub(txController, 'signTransaction')
.callsFake(() => Promise.resolve());
const pubStub = sinon
.stub(txController, 'publishTransaction')
.callsFake(() => {
txController.setTxHash('1', originalValue);
txController.txStateManager.setTxStatusSubmitted('1');
});
pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => {
txController.setTxHash('1', originalValue);
txController.txStateManager.setTxStatusSubmitted('1');
});
});
afterEach(function () {
signStub.restore();
pubStub.restore();
});
it('does not overwrite set values', async function () {
await txController.approveTransaction(txMeta.id);
const result = txController.txStateManager.getTransaction(txMeta.id);
const params = result.txParams;
@ -1042,8 +1116,21 @@ describe('Transaction Controller', function () {
TransactionStatus.submitted,
'should have reached the submitted status.',
);
signStub.restore();
pubStub.restore();
});
it('should accept the approval request', async function () {
await txController.approveTransaction(txMeta.id);
assert.equal(messengerMock.call.callCount, 1);
assert.deepEqual(messengerMock.call.getCall(0).args, [
'ApprovalController:acceptRequest',
txMeta.id,
]);
});
it('should not throw if accepting approval request throws', async function () {
messengerMock.call.throws();
await txController.approveTransaction(txMeta.id);
});
});
@ -1108,7 +1195,7 @@ describe('Transaction Controller', function () {
});
describe('#cancelTransaction', function () {
it('should emit a status change to rejected', function (done) {
beforeEach(function () {
txController.txStateManager._addTransactionsToState([
{
id: 0,
@ -1181,7 +1268,9 @@ describe('Transaction Controller', function () {
history: [{}],
},
]);
});
it('should emit a status change to rejected', function (done) {
txController.once('tx:status-update', (txId, status) => {
try {
assert.equal(
@ -1198,6 +1287,22 @@ describe('Transaction Controller', function () {
txController.cancelTransaction(0);
});
it('should reject the approval request', function () {
txController.cancelTransaction(0);
assert.equal(messengerMock.call.callCount, 1);
assert.deepEqual(messengerMock.call.getCall(0).args, [
'ApprovalController:rejectRequest',
'0',
new Error('Rejected'),
]);
});
it('should not throw if rejecting approval request throws', async function () {
messengerMock.call.throws();
txController.cancelTransaction(0);
});
});
describe('#createSpeedUpTransaction', function () {

View File

@ -1,318 +0,0 @@
import EventEmitter from 'events';
import { ObservableStore } from '@metamask/obs-store';
import { ethErrors } from 'eth-rpc-errors';
import log from 'loglevel';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import createId from '../../../shared/modules/random-id';
/**
* Represents, and contains data about, an 'eth_getEncryptionPublicKey' type request. These are created when
* an eth_getEncryptionPublicKey call is requested.
*
* @typedef {object} EncryptionPublicKey
* @property {number} id An id to track and identify the message object
* @property {object} msgParams The parameters to pass to the encryptionPublicKey method once the request is
* approved.
* @property {object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the request
* @property {number} time The epoch time at which the this message was created
* @property {string} status Indicates whether the request is 'unapproved', 'approved', 'received' or 'rejected'
* @property {string} type The json-prc method for which a request has been made. A 'Message' will
* always have a 'eth_getEncryptionPublicKey' type.
*/
export default class EncryptionPublicKeyManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - EncryptionPublicKey.
*
* @param {object} opts - Controller options
* @param {Function} opts.metricEvent - A function for emitting a metric event.
*/
constructor(opts) {
super();
this.memStore = new ObservableStore({
unapprovedEncryptionPublicKeyMsgs: {},
unapprovedEncryptionPublicKeyMsgCount: 0,
});
this.resetState = () => {
this.memStore.updateState({
unapprovedEncryptionPublicKeyMsgs: {},
unapprovedEncryptionPublicKeyMsgCount: 0,
});
};
this.messages = [];
this.metricsEvent = opts.metricsEvent;
}
/**
* A getter for the number of 'unapproved' EncryptionPublicKeys in this.messages
*
* @returns {number} The number of 'unapproved' EncryptionPublicKeys in this.messages
*/
get unapprovedEncryptionPublicKeyMsgCount() {
return Object.keys(this.getUnapprovedMsgs()).length;
}
/**
* A getter for the 'unapproved' EncryptionPublicKeys in this.messages
*
* @returns {object} An index of EncryptionPublicKey ids to EncryptionPublicKeys, for all 'unapproved' EncryptionPublicKeys in
* this.messages
*/
getUnapprovedMsgs() {
return this.messages
.filter((msg) => msg.status === 'unapproved')
.reduce((result, msg) => {
result[msg.id] = msg;
return result;
}, {});
}
/**
* Creates a new EncryptionPublicKey with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new EncryptionPublicKey to this.messages, and to save the unapproved EncryptionPublicKeys from that list to
* this.memStore.
*
* @param {object} address - The param for the eth_getEncryptionPublicKey call to be made after the message is approved.
* @param {object} [req] - The original request object possibly containing the origin
* @returns {Promise<Buffer>} The raw public key contents
*/
addUnapprovedMessageAsync(address, req) {
return new Promise((resolve, reject) => {
if (!address) {
reject(new Error('MetaMask Message: address field is required.'));
return;
}
const msgId = this.addUnapprovedMessage(address, req);
this.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'received':
resolve(data.rawData);
return;
case 'rejected':
reject(
ethErrors.provider.userRejectedRequest(
'MetaMask EncryptionPublicKey: User denied message EncryptionPublicKey.',
),
);
return;
default:
reject(
new Error(
`MetaMask EncryptionPublicKey: Unknown problem: ${JSON.stringify(
address,
)}`,
),
);
}
});
});
}
/**
* Creates a new EncryptionPublicKey with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new EncryptionPublicKey to this.messages, and to save the unapproved EncryptionPublicKeys from that list to
* this.memStore.
*
* @param {object} address - The param for the eth_getEncryptionPublicKey call to be made after the message is approved.
* @param {object} [req] - The original request object possibly containing the origin
* @returns {number} The id of the newly created EncryptionPublicKey.
*/
addUnapprovedMessage(address, req) {
log.debug(`EncryptionPublicKeyManager addUnapprovedMessage: address`);
// create txData obj with parameters and meta data
const time = new Date().getTime();
const msgId = createId();
const msgData = {
id: msgId,
msgParams: address,
time,
status: 'unapproved',
type: MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY,
};
if (req) {
msgData.origin = req.origin;
}
this.addMsg(msgData);
// signal update
this.emit('update');
return msgId;
}
/**
* Adds a passed EncryptionPublicKey to this.messages, and calls this._saveMsgList() to save the unapproved EncryptionPublicKeys from that
* list to this.memStore.
*
* @param {Message} msg - The EncryptionPublicKey to add to this.messages
*/
addMsg(msg) {
this.messages.push(msg);
this._saveMsgList();
}
/**
* Returns a specified EncryptionPublicKey.
*
* @param {number} msgId - The id of the EncryptionPublicKey to get
* @returns {EncryptionPublicKey|undefined} The EncryptionPublicKey with the id that matches the passed msgId, or undefined
* if no EncryptionPublicKey has that id.
*/
getMsg(msgId) {
return this.messages.find((msg) => msg.id === msgId);
}
/**
* Approves a EncryptionPublicKey. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise
* with any the message params modified for proper providing.
*
* @param {object} msgParams - The msgParams to be used when eth_getEncryptionPublicKey is called, plus data added by MetaMask.
* @param {object} msgParams.metamaskId - Added to msgParams for tracking and identification within MetaMask.
* @returns {Promise<object>} Promises the msgParams object with metamaskId removed.
*/
approveMessage(msgParams) {
this.setMsgStatusApproved(msgParams.metamaskId);
return this.prepMsgForEncryptionPublicKey(msgParams);
}
/**
* Sets a EncryptionPublicKey status to 'approved' via a call to this._setMsgStatus.
*
* @param {number} msgId - The id of the EncryptionPublicKey to approve.
*/
setMsgStatusApproved(msgId) {
this._setMsgStatus(msgId, 'approved');
}
/**
* Sets a EncryptionPublicKey status to 'received' via a call to this._setMsgStatus and updates that EncryptionPublicKey in
* this.messages by adding the raw data of request to the EncryptionPublicKey
*
* @param {number} msgId - The id of the EncryptionPublicKey.
* @param {buffer} rawData - The raw data of the message request
*/
setMsgStatusReceived(msgId, rawData) {
const msg = this.getMsg(msgId);
msg.rawData = rawData;
this._updateMsg(msg);
this._setMsgStatus(msgId, 'received');
}
/**
* Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams
*
* @param {object} msgParams - The msgParams to modify
* @returns {Promise<object>} Promises the msgParams with the metamaskId property removed
*/
async prepMsgForEncryptionPublicKey(msgParams) {
delete msgParams.metamaskId;
return msgParams;
}
/**
* Sets a EncryptionPublicKey status to 'rejected' via a call to this._setMsgStatus.
*
* @param {number} msgId - The id of the EncryptionPublicKey to reject.
* @param reason
*/
rejectMsg(msgId, reason = undefined) {
if (reason) {
this.metricsEvent({
event: reason,
category: MetaMetricsEventCategory.Messages,
properties: {
action: 'Encryption public key Request',
},
});
}
this._setMsgStatus(msgId, 'rejected');
}
/**
* Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus.
*
* @param {number} msgId - The id of the TypedMessage to error
* @param error
*/
errorMessage(msgId, error) {
const msg = this.getMsg(msgId);
msg.error = error;
this._updateMsg(msg);
this._setMsgStatus(msgId, 'errored');
}
/**
* Clears all unapproved messages from memory.
*/
clearUnapproved() {
this.messages = this.messages.filter((msg) => msg.status !== 'unapproved');
this._saveMsgList();
}
/**
* Updates the status of a EncryptionPublicKey in this.messages via a call to this._updateMsg
*
* @private
* @param {number} msgId - The id of the EncryptionPublicKey to update.
* @param {string} status - The new status of the EncryptionPublicKey.
* @throws A 'EncryptionPublicKeyManager - EncryptionPublicKey not found for id: "${msgId}".' if there is no EncryptionPublicKey
* in this.messages with an id equal to the passed msgId
* @fires An event with a name equal to `${msgId}:${status}`. The EncryptionPublicKey is also fired.
* @fires If status is 'rejected' or 'received', an event with a name equal to `${msgId}:finished` is fired along
* with the EncryptionPublicKey
*/
_setMsgStatus(msgId, status) {
const msg = this.getMsg(msgId);
if (!msg) {
throw new Error(
`EncryptionPublicKeyManager - Message not found for id: "${msgId}".`,
);
}
msg.status = status;
this._updateMsg(msg);
this.emit(`${msgId}:${status}`, msg);
if (status === 'rejected' || status === 'received') {
this.emit(`${msgId}:finished`, msg);
}
}
/**
* Sets a EncryptionPublicKey in this.messages to the passed EncryptionPublicKey if the ids are equal. Then saves the
* unapprovedEncryptionPublicKeyMsgs index to storage via this._saveMsgList
*
* @private
* @param {EncryptionPublicKey} msg - A EncryptionPublicKey that will replace an existing EncryptionPublicKey (with the same
* id) in this.messages
*/
_updateMsg(msg) {
const index = this.messages.findIndex((message) => message.id === msg.id);
if (index !== -1) {
this.messages[index] = msg;
}
this._saveMsgList();
}
/**
* Saves the unapproved EncryptionPublicKeys, and their count, to this.memStore
*
* @private
* @fires 'updateBadge'
*/
_saveMsgList() {
const unapprovedEncryptionPublicKeyMsgs = this.getUnapprovedMsgs();
const unapprovedEncryptionPublicKeyMsgCount = Object.keys(
unapprovedEncryptionPublicKeyMsgs,
).length;
this.memStore.updateState({
unapprovedEncryptionPublicKeyMsgs,
unapprovedEncryptionPublicKeyMsgCount,
});
this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE);
}
}

View File

@ -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,
@ -143,8 +143,9 @@ import createTabIdMiddleware from './lib/createTabIdMiddleware';
import createOnboardingMiddleware from './lib/createOnboardingMiddleware';
import { setupMultiplex } from './lib/stream-utils';
import EnsController from './controllers/ens';
import NetworkController, {
NetworkControllerEventTypes,
import {
NetworkController,
NetworkControllerEventType,
} from './controllers/network';
import PreferencesController from './controllers/preferences';
import AppStateController from './controllers/app-state';
@ -154,7 +155,6 @@ import OnboardingController from './controllers/onboarding';
import BackupController from './controllers/backup';
import IncomingTransactionsController from './controllers/incoming-transactions';
import DecryptMessageManager from './lib/decrypt-message-manager';
import EncryptionPublicKeyManager from './lib/encryption-public-key-manager';
import TransactionController from './controllers/transactions';
import DetectTokensController from './controllers/detect-tokens';
import SwapsController from './controllers/swaps';
@ -166,6 +166,7 @@ import createMetaRPCHandler from './lib/createMetaRPCHandler';
import { previousValueComparator } from './lib/util';
import createMetamaskMiddleware from './lib/createMetamaskMiddleware';
import SignController from './controllers/sign';
import EncryptionPublicKeyController from './controllers/encryption-public-key';
import {
CaveatMutatorFactories,
@ -259,11 +260,16 @@ 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({
name: 'NetworkController',
allowedEvents: Object.values(NetworkControllerEventTypes),
allowedEvents: Object.values(NetworkControllerEventType),
});
this.networkController = new NetworkController({
messenger: networkControllerMessenger,
@ -310,11 +316,11 @@ export default class MetamaskController extends EventEmitter {
initLangCode: opts.initLangCode,
onInfuraIsBlocked: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.InfuraIsBlocked,
NetworkControllerEventType.InfuraIsBlocked,
),
onInfuraIsUnblocked: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.InfuraIsUnblocked,
NetworkControllerEventType.InfuraIsUnblocked,
),
tokenListController: this.tokenListController,
provider: this.provider,
@ -452,7 +458,7 @@ export default class MetamaskController extends EventEmitter {
preferencesStore: this.preferencesController.store,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
getNetworkIdentifier: () => {
const { type, rpcUrl } =
@ -491,7 +497,7 @@ export default class MetamaskController extends EventEmitter {
// onNetworkDidChange
onNetworkStateChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
getCurrentNetworkEIP1559Compatibility:
this.networkController.getEIP1559Compatibility.bind(
@ -519,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({
@ -609,7 +621,7 @@ export default class MetamaskController extends EventEmitter {
this.networkController.store.getState().provider.chainId,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
});
@ -621,7 +633,7 @@ export default class MetamaskController extends EventEmitter {
blockTracker: this.blockTracker,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
@ -1007,8 +1019,15 @@ export default class MetamaskController extends EventEmitter {
getDeviceModel: this.getDeviceModel.bind(this),
getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this),
securityProviderRequest: this.securityProviderRequest.bind(this),
messenger: this.controllerMessenger.getRestricted({
name: 'TransactionController',
allowedActions: [
`${this.approvalController.name}:addRequest`,
`${this.approvalController.name}:acceptRequest`,
`${this.approvalController.name}:rejectRequest`,
],
}),
});
this.txController.on('newUnapprovedTx', () => opts.showUserConfirmation());
this.txController.on(`tx:status-update`, async (txId, status) => {
if (
@ -1099,7 +1118,7 @@ export default class MetamaskController extends EventEmitter {
});
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
async () => {
const { ticker } = this.networkController.store.getState().provider;
try {
@ -1117,7 +1136,18 @@ export default class MetamaskController extends EventEmitter {
this.metaMetricsController,
),
});
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager({
this.encryptionPublicKeyController = new EncryptionPublicKeyController({
messenger: this.controllerMessenger.getRestricted({
name: 'EncryptionPublicKeyController',
allowedActions: [
`${this.approvalController.name}:addRequest`,
`${this.approvalController.name}:acceptRequest`,
`${this.approvalController.name}:rejectRequest`,
],
}),
keyringController: this.keyringController,
getState: this.getState.bind(this),
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
@ -1145,7 +1175,7 @@ export default class MetamaskController extends EventEmitter {
networkController: this.networkController,
onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
),
provider: this.provider,
getProviderConfig: () => this.networkController.store.getState().provider,
@ -1188,7 +1218,7 @@ export default class MetamaskController extends EventEmitter {
// ensure accountTracker updates balances after network change
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange,
NetworkControllerEventType.NetworkDidChange,
() => {
this.accountTracker._updateAccounts();
},
@ -1196,10 +1226,10 @@ export default class MetamaskController extends EventEmitter {
// clear unapproved transactions and messages when the network will change
networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkWillChange,
NetworkControllerEventType.NetworkWillChange,
() => {
this.txController.txStateManager.clearUnapprovedTxs();
this.encryptionPublicKeyManager.clearUnapproved();
this.encryptionPublicKeyController.clearUnapproved();
this.decryptMessageManager.clearUnapproved();
this.signController.clearUnapproved();
},
@ -1266,7 +1296,10 @@ export default class MetamaskController extends EventEmitter {
this.signController,
),
processDecryptMessage: this.newRequestDecryptMessage.bind(this),
processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this),
processEncryptionPublicKey:
this.encryptionPublicKeyController.newRequestEncryptionPublicKey.bind(
this.encryptionPublicKeyController,
),
getPendingNonce: this.getPendingNonce.bind(this),
getPendingTransactionByHash: (hash) =>
this.txController.getTransactions({
@ -1289,7 +1322,7 @@ export default class MetamaskController extends EventEmitter {
TxController: this.txController.memStore,
TokenRatesController: this.tokenRatesController,
DecryptMessageManager: this.decryptMessageManager.memStore,
EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore,
EncryptionPublicKeyController: this.encryptionPublicKeyController,
SignController: this.signController,
SwapsController: this.swapsController.store,
EnsController: this.ensController.store,
@ -1371,7 +1404,9 @@ export default class MetamaskController extends EventEmitter {
this.accountTracker.resetState,
this.txController.resetState,
this.decryptMessageManager.resetState,
this.encryptionPublicKeyManager.resetState,
this.encryptionPublicKeyController.resetState.bind(
this.encryptionPublicKeyController,
),
this.signController.resetState.bind(this.signController),
this.swapsController.resetState,
this.ensController.resetState,
@ -1994,6 +2029,16 @@ export default class MetamaskController extends EventEmitter {
appStateController.setConnectedStatusPopoverHasBeenShown.bind(
appStateController,
),
setRecoveryPhraseReminderHasBeenShown:
appStateController.setRecoveryPhraseReminderHasBeenShown.bind(
appStateController,
),
setRecoveryPhraseReminderLastShown:
appStateController.setRecoveryPhraseReminderLastShown.bind(
appStateController,
),
setTermsOfUseLastAgreed:
appStateController.setTermsOfUseLastAgreed.bind(appStateController),
setOutdatedBrowserWarningLastShown:
appStateController.setOutdatedBrowserWarningLastShown.bind(
appStateController,
@ -2074,9 +2119,15 @@ export default class MetamaskController extends EventEmitter {
decryptMessageInline: this.decryptMessageInline.bind(this),
cancelDecryptMessage: this.cancelDecryptMessage.bind(this),
// EncryptionPublicKeyManager
encryptionPublicKey: this.encryptionPublicKey.bind(this),
cancelEncryptionPublicKey: this.cancelEncryptionPublicKey.bind(this),
// EncryptionPublicKeyController
encryptionPublicKey:
this.encryptionPublicKeyController.encryptionPublicKey.bind(
this.encryptionPublicKeyController,
),
cancelEncryptionPublicKey:
this.encryptionPublicKeyController.cancelEncryptionPublicKey.bind(
this.encryptionPublicKeyController,
),
// onboarding controller
setSeedPhraseBackedUp:
@ -3301,109 +3352,6 @@ export default class MetamaskController extends EventEmitter {
return this.getState();
}
// eth_getEncryptionPublicKey methods
/**
* Called when a dapp uses the eth_getEncryptionPublicKey method.
*
* @param {object} msgParams - The params of the message to sign & return to the Dapp.
* @param {object} req - (optional) the original request, containing the origin
* Passed back to the requesting Dapp.
*/
async newRequestEncryptionPublicKey(msgParams, req) {
const address = msgParams;
const keyring = await this.keyringController.getKeyringForAccount(address);
switch (keyring.type) {
case KeyringType.ledger: {
return new Promise((_, reject) => {
reject(
new Error('Ledger does not support eth_getEncryptionPublicKey.'),
);
});
}
case KeyringType.trezor: {
return new Promise((_, reject) => {
reject(
new Error('Trezor does not support eth_getEncryptionPublicKey.'),
);
});
}
case KeyringType.lattice: {
return new Promise((_, reject) => {
reject(
new Error('Lattice does not support eth_getEncryptionPublicKey.'),
);
});
}
case KeyringType.qr: {
return Promise.reject(
new Error('QR hardware does not support eth_getEncryptionPublicKey.'),
);
}
default: {
const promise =
this.encryptionPublicKeyManager.addUnapprovedMessageAsync(
msgParams,
req,
);
this.sendUpdate();
this.opts.showUserConfirmation();
return promise;
}
}
}
/**
* Signifies a user's approval to receiving encryption public key in queue.
* Triggers receiving, and the callback function from newUnsignedEncryptionPublicKey.
*
* @param {object} msgParams - The params of the message to receive & return to the Dapp.
* @returns {Promise<object>} A full state update.
*/
async encryptionPublicKey(msgParams) {
log.info('MetaMaskController - encryptionPublicKey');
const msgId = msgParams.metamaskId;
// sets the status op the message to 'approved'
// and removes the metamaskId for decryption
try {
const params = await this.encryptionPublicKeyManager.approveMessage(
msgParams,
);
// EncryptionPublicKey message
const publicKey = await this.keyringController.getEncryptionPublicKey(
params.data,
);
// tells the listener that the message has been processed
// and can be returned to the dapp
this.encryptionPublicKeyManager.setMsgStatusReceived(msgId, publicKey);
} catch (error) {
log.info(
'MetaMaskController - eth_getEncryptionPublicKey failed.',
error,
);
this.encryptionPublicKeyManager.errorMessage(msgId, error);
}
return this.getState();
}
/**
* Used to cancel a eth_getEncryptionPublicKey type message.
*
* @param {string} msgId - The ID of the message to cancel.
*/
cancelEncryptionPublicKey(msgId) {
const messageManager = this.encryptionPublicKeyManager;
messageManager.rejectMsg(msgId);
return this.getState();
}
/**
* @returns {boolean} true if the keyring type supports EIP-1559
*/

View File

@ -0,0 +1,254 @@
import { v4 } from 'uuid';
import { migrate, version } from './084';
jest.mock('uuid', () => {
const actual = jest.requireActual('uuid');
return {
...actual,
v4: jest.fn(),
};
});
describe('migration #84', () => {
beforeEach(() => {
v4.mockImplementationOnce(() => 'network-configuration-id-1')
.mockImplementationOnce(() => 'network-configuration-id-2')
.mockImplementationOnce(() => 'network-configuration-id-3')
.mockImplementationOnce(() => 'network-configuration-id-4');
});
afterEach(() => {
jest.resetAllMocks();
});
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
version: 83,
},
data: {},
};
const newStorage = await migrate(oldStorage);
expect(newStorage.meta).toStrictEqual({
version,
});
});
it('should use the key of the networkConfigurations object to set the id of each network configuration', async () => {
const oldStorage = {
meta: {
version,
},
data: {
NetworkController: {
networkConfigurations: {
'network-configuration-id-1': {
chainId: '0x539',
nickname: 'Localhost 8545',
rpcPrefs: {},
rpcUrl: 'http://localhost:8545',
ticker: 'ETH',
},
'network-configuration-id-2': {
chainId: '0xa4b1',
nickname: 'Arbitrum One',
rpcPrefs: {
blockExplorerUrl: 'https://explorer.arbitrum.io',
},
rpcUrl:
'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
ticker: 'ETH',
},
'network-configuration-id-3': {
chainId: '0x4e454152',
nickname: 'Aurora Mainnet',
rpcPrefs: {
blockExplorerUrl: 'https://aurorascan.dev/',
},
rpcUrl:
'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
ticker: 'Aurora ETH',
},
'network-configuration-id-4': {
chainId: '0x38',
nickname:
'BNB Smart Chain (previously Binance Smart Chain Mainnet)',
rpcPrefs: {
blockExplorerUrl: 'https://bscscan.com/',
},
rpcUrl: 'https://bsc-dataseed.binance.org/',
ticker: 'BNB',
},
},
},
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
NetworkController: {
networkConfigurations: {
'network-configuration-id-1': {
chainId: '0x539',
nickname: 'Localhost 8545',
rpcPrefs: {},
rpcUrl: 'http://localhost:8545',
ticker: 'ETH',
id: 'network-configuration-id-1',
},
'network-configuration-id-2': {
chainId: '0xa4b1',
nickname: 'Arbitrum One',
rpcPrefs: {
blockExplorerUrl: 'https://explorer.arbitrum.io',
},
rpcUrl:
'https://arbitrum-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
ticker: 'ETH',
id: 'network-configuration-id-2',
},
'network-configuration-id-3': {
chainId: '0x4e454152',
nickname: 'Aurora Mainnet',
rpcPrefs: {
blockExplorerUrl: 'https://aurorascan.dev/',
},
rpcUrl:
'https://aurora-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
ticker: 'Aurora ETH',
id: 'network-configuration-id-3',
},
'network-configuration-id-4': {
chainId: '0x38',
nickname:
'BNB Smart Chain (previously Binance Smart Chain Mainnet)',
rpcPrefs: {
blockExplorerUrl: 'https://bscscan.com/',
},
rpcUrl: 'https://bsc-dataseed.binance.org/',
ticker: 'BNB',
id: 'network-configuration-id-4',
},
},
},
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
it('should not modify state if state.NetworkController is undefined', async () => {
const oldStorage = {
meta: {
version,
},
data: {
testProperty: 'testValue',
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
testProperty: 'testValue',
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
it('should not modify state if state.NetworkController is not an object', async () => {
const oldStorage = {
meta: {
version,
},
data: {
NetworkController: false,
testProperty: 'testValue',
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
NetworkController: false,
testProperty: 'testValue',
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
it('should not modify state if state.NetworkController.networkConfigurations is undefined', async () => {
const oldStorage = {
meta: {
version,
},
data: {
NetworkController: {
testNetworkControllerProperty: 'testNetworkControllerValue',
networkConfigurations: undefined,
},
testProperty: 'testValue',
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
NetworkController: {
testNetworkControllerProperty: 'testNetworkControllerValue',
networkConfigurations: undefined,
},
testProperty: 'testValue',
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
it('should not modify state if state.NetworkController.networkConfigurations is an empty object', async () => {
const oldStorage = {
meta: {
version,
},
data: {
NetworkController: {
testNetworkControllerProperty: 'testNetworkControllerValue',
networkConfigurations: {},
},
testProperty: 'testValue',
},
};
const newStorage = await migrate(oldStorage);
const expectedNewStorage = {
meta: {
version,
},
data: {
NetworkController: {
testNetworkControllerProperty: 'testNetworkControllerValue',
networkConfigurations: {},
},
testProperty: 'testValue',
},
};
expect(newStorage).toStrictEqual(expectedNewStorage);
});
});

View File

@ -0,0 +1,58 @@
import { cloneDeep } from 'lodash';
import { isObject } from '@metamask/utils';
export const version = 84;
/**
* Ensure that each networkConfigurations object in state.NetworkController.networkConfigurations has an
* `id` property which matches the key pointing that object
*
* @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<string, unknown>;
}) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
versionedData.data = transformState(versionedData.data);
return versionedData;
}
function transformState(state: Record<string, unknown>) {
if (!isObject(state.NetworkController)) {
return state;
}
const { NetworkController } = state;
if (!isObject(NetworkController.networkConfigurations)) {
return state;
}
const { networkConfigurations } = NetworkController;
const newNetworkConfigurations: Record<string, Record<string, unknown>> = {};
for (const networkConfigurationId of Object.keys(networkConfigurations)) {
const networkConfiguration = networkConfigurations[networkConfigurationId];
if (!isObject(networkConfiguration)) {
return state;
}
newNetworkConfigurations[networkConfigurationId] = {
...networkConfiguration,
id: networkConfigurationId,
};
}
return {
...state,
NetworkController: {
...NetworkController,
networkConfigurations: newNetworkConfigurations,
},
};
}

View File

@ -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',
},
},
});
});
});

View File

@ -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<string, unknown>;
}) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
versionedData.data = transformState(versionedData.data);
return versionedData;
}
function transformState(state: Record<string, unknown>) {
if (!isObject(state.NetworkController)) {
return state;
}
delete state.NetworkController.previousProviderStore;
return state;
}

View File

@ -87,6 +87,8 @@ import m080 from './080';
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,
@ -171,6 +173,8 @@ const migrations = [
m081,
m082,
m083,
m084,
m085,
];
export default migrations;

View File

@ -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,

View File

@ -2,6 +2,7 @@ module.exports = {
collectCoverageFrom: [
'<rootDir>/app/scripts/constants/error-utils.js',
'<rootDir>/app/scripts/controllers/network/**/*.js',
'<rootDir>/app/scripts/controllers/network/**/*.ts',
'<rootDir>/app/scripts/controllers/permissions/**/*.js',
'<rootDir>/app/scripts/controllers/sign.ts',
'<rootDir>/app/scripts/flask/**/*.js',
@ -39,6 +40,7 @@ module.exports = {
'<rootDir>/app/scripts/constants/error-utils.test.js',
'<rootDir>/app/scripts/controllers/app-state.test.js',
'<rootDir>/app/scripts/controllers/network/**/*.test.js',
'<rootDir>/app/scripts/controllers/network/**/*.test.ts',
'<rootDir>/app/scripts/controllers/permissions/**/*.test.js',
'<rootDir>/app/scripts/controllers/sign.test.ts',
'<rootDir>/app/scripts/flask/**/*.test.js',
@ -51,7 +53,7 @@ module.exports = {
'<rootDir>/ui/**/*.test.(js|ts|tsx)',
'<rootDir>/development/fitness-functions/**/*.test.(js|ts|tsx)',
],
testTimeout: 2500,
testTimeout: 5500,
// We have to specify the environment we are running in, which is jsdom. The
// default is 'node'. This can be modified *per file* using a comment at the
// head of the file. So it may be worthwhile to switch to 'node' in any

View File

@ -660,28 +660,8 @@
},
"@metamask/address-book-controller": {
"packages": {
"@metamask/address-book-controller>@metamask/base-controller": true,
"@metamask/address-book-controller>@metamask/controller-utils": true
}
},
"@metamask/address-book-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/address-book-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
"@metamask/base-controller": true,
"@metamask/controller-utils": true
}
},
"@metamask/announcement-controller": {
@ -716,12 +696,12 @@
"@ethersproject/contracts": true,
"@ethersproject/providers": true,
"@metamask/assets-controllers>@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/controller-utils": true,
"@metamask/assets-controllers>@metamask/utils": true,
"@metamask/assets-controllers>abort-controller": true,
"@metamask/assets-controllers>multiformats": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/metamask-eth-abis": true,
"browserify>events": true,
"eth-json-rpc-filters>async-mutex": true,
@ -750,21 +730,6 @@
"semver": true
}
},
"@metamask/assets-controllers>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/assets-controllers>@metamask/utils": {
"globals": {
"TextDecoder": true,
@ -1089,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
}
},
@ -1193,34 +1158,14 @@
"setInterval": true
},
"packages": {
"@metamask/gas-fee-controller>@metamask/base-controller": true,
"@metamask/gas-fee-controller>@metamask/controller-utils": true,
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"eth-query": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true,
"uuid": true
}
},
"@metamask/gas-fee-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/gas-fee-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/jazzicon": {
"globals": {
"document.createElement": true,
@ -1319,8 +1264,8 @@
},
"@metamask/message-manager": {
"packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/message-manager>@metamask/base-controller": true,
"@metamask/message-manager>jsonschema": true,
"browserify>buffer": true,
"browserify>events": true,
@ -1329,11 +1274,6 @@
"uuid": true
}
},
"@metamask/message-manager>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/message-manager>jsonschema": {
"packages": {
"browserify>url": true
@ -1364,8 +1304,8 @@
"console.error": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/permission-controller>nanoid": true,
"deep-freeze-strict": true,
"eth-rpc-errors": true,
@ -1373,11 +1313,6 @@
"json-rpc-engine": true
}
},
"@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/permission-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1389,46 +1324,11 @@
},
"packages": {
"@metamask/base-controller": true,
"@metamask/phishing-controller>@metamask/controller-utils": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/controller-utils": true,
"@metamask/phishing-warning>eth-phishing-detect": true,
"punycode": true
}
},
"@metamask/phishing-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/phishing-controller>isomorphic-fetch": {
"globals": {
"fetch.bind": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true
}
},
"@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": {
"globals": {
"Blob": true,
"FileReader": true,
"FormData": true,
"URLSearchParams.prototype.isPrototypeOf": true,
"XMLHttpRequest": true,
"define": true,
"setTimeout": true
}
},
"@metamask/phishing-warning>eth-phishing-detect": {
"packages": {
"eslint>optionator>fast-levenshtein": true
@ -1491,14 +1391,19 @@
"@ethersproject/abi>@ethersproject/bytes": true,
"@ethersproject/bignumber": true,
"@ethersproject/providers": true,
"@metamask/base-controller": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>@metamask/base-controller": true,
"@metamask/smart-transactions-controller>@metamask/controller-utils": true,
"@metamask/smart-transactions-controller>bignumber.js": true,
"@metamask/smart-transactions-controller>isomorphic-fetch": true,
"fast-json-patch": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/smart-transactions-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
@ -1506,7 +1411,7 @@
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1525,6 +1430,25 @@
"define": true
}
},
"@metamask/smart-transactions-controller>isomorphic-fetch": {
"globals": {
"fetch.bind": true
},
"packages": {
"@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": true
}
},
"@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": {
"globals": {
"Blob": true,
"FileReader": true,
"FormData": true,
"URLSearchParams.prototype.isPrototypeOf": true,
"XMLHttpRequest": true,
"define": true,
"setTimeout": true
}
},
"@metamask/snaps-controllers>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -2560,7 +2484,7 @@
},
"browserify>has": {
"packages": {
"mocha>object.assign>function-bind": true
"browserify>has>function-bind": true
}
},
"browserify>os-browserify": {
@ -3615,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": {
@ -4235,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
}
},
@ -4247,7 +4171,7 @@
},
"packages": {
"browserify>has": true,
"mocha>object.assign>function-bind": true,
"browserify>has>function-bind": true,
"string.prototype.matchall>has-symbols": true
}
},

View File

@ -660,28 +660,8 @@
},
"@metamask/address-book-controller": {
"packages": {
"@metamask/address-book-controller>@metamask/base-controller": true,
"@metamask/address-book-controller>@metamask/controller-utils": true
}
},
"@metamask/address-book-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/address-book-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
"@metamask/base-controller": true,
"@metamask/controller-utils": true
}
},
"@metamask/announcement-controller": {
@ -716,12 +696,12 @@
"@ethersproject/contracts": true,
"@ethersproject/providers": true,
"@metamask/assets-controllers>@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/controller-utils": true,
"@metamask/assets-controllers>@metamask/utils": true,
"@metamask/assets-controllers>abort-controller": true,
"@metamask/assets-controllers>multiformats": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/metamask-eth-abis": true,
"browserify>events": true,
"eth-json-rpc-filters>async-mutex": true,
@ -750,21 +730,6 @@
"semver": true
}
},
"@metamask/assets-controllers>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/assets-controllers>@metamask/utils": {
"globals": {
"TextDecoder": true,
@ -1161,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
}
},
@ -1265,34 +1230,14 @@
"setInterval": true
},
"packages": {
"@metamask/gas-fee-controller>@metamask/base-controller": true,
"@metamask/gas-fee-controller>@metamask/controller-utils": true,
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"eth-query": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true,
"uuid": true
}
},
"@metamask/gas-fee-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/gas-fee-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/jazzicon": {
"globals": {
"document.createElement": true,
@ -1391,8 +1336,8 @@
},
"@metamask/message-manager": {
"packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/message-manager>@metamask/base-controller": true,
"@metamask/message-manager>jsonschema": true,
"browserify>buffer": true,
"browserify>events": true,
@ -1401,11 +1346,6 @@
"uuid": true
}
},
"@metamask/message-manager>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/message-manager>jsonschema": {
"packages": {
"browserify>url": true
@ -1413,31 +1353,11 @@
},
"@metamask/notification-controller": {
"packages": {
"@metamask/notification-controller>@metamask/base-controller": true,
"@metamask/notification-controller>@metamask/controller-utils": true,
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/notification-controller>nanoid": true
}
},
"@metamask/notification-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/notification-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/notification-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1463,8 +1383,8 @@
"console.error": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/permission-controller>nanoid": true,
"deep-freeze-strict": true,
"eth-rpc-errors": true,
@ -1472,11 +1392,6 @@
"json-rpc-engine": true
}
},
"@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/permission-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1488,46 +1403,11 @@
},
"packages": {
"@metamask/base-controller": true,
"@metamask/phishing-controller>@metamask/controller-utils": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/controller-utils": true,
"@metamask/phishing-warning>eth-phishing-detect": true,
"punycode": true
}
},
"@metamask/phishing-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/phishing-controller>isomorphic-fetch": {
"globals": {
"fetch.bind": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true
}
},
"@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": {
"globals": {
"Blob": true,
"FileReader": true,
"FormData": true,
"URLSearchParams.prototype.isPrototypeOf": true,
"XMLHttpRequest": true,
"define": true,
"setTimeout": true
}
},
"@metamask/phishing-warning>eth-phishing-detect": {
"packages": {
"eslint>optionator>fast-levenshtein": true
@ -1607,15 +1487,10 @@
"setTimeout": true
},
"packages": {
"@metamask/rate-limit-controller>@metamask/base-controller": true,
"@metamask/base-controller": true,
"eth-rpc-errors": true
}
},
"@metamask/rate-limit-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/rpc-methods": {
"packages": {
"@metamask/key-tree": true,
@ -1679,14 +1554,19 @@
"@ethersproject/abi>@ethersproject/bytes": true,
"@ethersproject/bignumber": true,
"@ethersproject/providers": true,
"@metamask/base-controller": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>@metamask/base-controller": true,
"@metamask/smart-transactions-controller>@metamask/controller-utils": true,
"@metamask/smart-transactions-controller>bignumber.js": true,
"@metamask/smart-transactions-controller>isomorphic-fetch": true,
"fast-json-patch": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/smart-transactions-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
@ -1694,7 +1574,7 @@
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1713,6 +1593,25 @@
"define": true
}
},
"@metamask/smart-transactions-controller>isomorphic-fetch": {
"globals": {
"fetch.bind": true
},
"packages": {
"@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": true
}
},
"@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": {
"globals": {
"Blob": true,
"FileReader": true,
"FormData": true,
"URLSearchParams.prototype.isPrototypeOf": true,
"XMLHttpRequest": true,
"define": true,
"setTimeout": true
}
},
"@metamask/snaps-controllers": {
"globals": {
"URL": true,
@ -1724,12 +1623,11 @@
"setTimeout": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/permission-controller": true,
"@metamask/post-message-stream": true,
"@metamask/providers>@metamask/object-multiplex": true,
"@metamask/rpc-methods": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/subject-metadata-controller": true,
"@metamask/snaps-controllers>@xstate/fsm": true,
"@metamask/snaps-controllers>concat-stream": true,
"@metamask/snaps-controllers>gunzip-maybe": true,
@ -1738,6 +1636,7 @@
"@metamask/snaps-controllers>tar-stream": true,
"@metamask/snaps-utils": true,
"@metamask/snaps-utils>@metamask/snaps-registry": true,
"@metamask/subject-metadata-controller": true,
"@metamask/utils": true,
"eth-rpc-errors": true,
"json-rpc-engine": true,
@ -1745,16 +1644,6 @@
"pump": true
}
},
"@metamask/snaps-controllers>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/snaps-controllers>@metamask/subject-metadata-controller": {
"packages": {
"@metamask/snaps-controllers>@metamask/base-controller": true
}
},
"@metamask/snaps-controllers>concat-stream": {
"packages": {
"@metamask/snaps-controllers>concat-stream>readable-stream": true,
@ -2988,7 +2877,7 @@
},
"browserify>has": {
"packages": {
"mocha>object.assign>function-bind": true
"browserify>has>function-bind": true
}
},
"browserify>os-browserify": {
@ -4043,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": {
@ -4457,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
}
@ -4795,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
}
},
@ -4807,7 +4696,7 @@
},
"packages": {
"browserify>has": true,
"mocha>object.assign>function-bind": true,
"browserify>has>function-bind": true,
"string.prototype.matchall>has-symbols": true
}
},

View File

@ -660,28 +660,8 @@
},
"@metamask/address-book-controller": {
"packages": {
"@metamask/address-book-controller>@metamask/base-controller": true,
"@metamask/address-book-controller>@metamask/controller-utils": true
}
},
"@metamask/address-book-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/address-book-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
"@metamask/base-controller": true,
"@metamask/controller-utils": true
}
},
"@metamask/announcement-controller": {
@ -716,12 +696,12 @@
"@ethersproject/contracts": true,
"@ethersproject/providers": true,
"@metamask/assets-controllers>@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/controller-utils": true,
"@metamask/assets-controllers>@metamask/utils": true,
"@metamask/assets-controllers>abort-controller": true,
"@metamask/assets-controllers>multiformats": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/metamask-eth-abis": true,
"browserify>events": true,
"eth-json-rpc-filters>async-mutex": true,
@ -750,21 +730,6 @@
"semver": true
}
},
"@metamask/assets-controllers>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/assets-controllers>@metamask/utils": {
"globals": {
"TextDecoder": true,
@ -1161,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
}
},
@ -1265,34 +1230,14 @@
"setInterval": true
},
"packages": {
"@metamask/gas-fee-controller>@metamask/base-controller": true,
"@metamask/gas-fee-controller>@metamask/controller-utils": true,
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"eth-query": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true,
"uuid": true
}
},
"@metamask/gas-fee-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/gas-fee-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/jazzicon": {
"globals": {
"document.createElement": true,
@ -1391,8 +1336,8 @@
},
"@metamask/message-manager": {
"packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/message-manager>@metamask/base-controller": true,
"@metamask/message-manager>jsonschema": true,
"browserify>buffer": true,
"browserify>events": true,
@ -1401,11 +1346,6 @@
"uuid": true
}
},
"@metamask/message-manager>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/message-manager>jsonschema": {
"packages": {
"browserify>url": true
@ -1413,31 +1353,11 @@
},
"@metamask/notification-controller": {
"packages": {
"@metamask/notification-controller>@metamask/base-controller": true,
"@metamask/notification-controller>@metamask/controller-utils": true,
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/notification-controller>nanoid": true
}
},
"@metamask/notification-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/notification-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/notification-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1463,8 +1383,8 @@
"console.error": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/permission-controller>nanoid": true,
"deep-freeze-strict": true,
"eth-rpc-errors": true,
@ -1472,11 +1392,6 @@
"json-rpc-engine": true
}
},
"@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/permission-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1488,46 +1403,11 @@
},
"packages": {
"@metamask/base-controller": true,
"@metamask/phishing-controller>@metamask/controller-utils": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/controller-utils": true,
"@metamask/phishing-warning>eth-phishing-detect": true,
"punycode": true
}
},
"@metamask/phishing-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/phishing-controller>isomorphic-fetch": {
"globals": {
"fetch.bind": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true
}
},
"@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": {
"globals": {
"Blob": true,
"FileReader": true,
"FormData": true,
"URLSearchParams.prototype.isPrototypeOf": true,
"XMLHttpRequest": true,
"define": true,
"setTimeout": true
}
},
"@metamask/phishing-warning>eth-phishing-detect": {
"packages": {
"eslint>optionator>fast-levenshtein": true
@ -1607,15 +1487,10 @@
"setTimeout": true
},
"packages": {
"@metamask/rate-limit-controller>@metamask/base-controller": true,
"@metamask/base-controller": true,
"eth-rpc-errors": true
}
},
"@metamask/rate-limit-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/rpc-methods": {
"packages": {
"@metamask/key-tree": true,
@ -1679,14 +1554,19 @@
"@ethersproject/abi>@ethersproject/bytes": true,
"@ethersproject/bignumber": true,
"@ethersproject/providers": true,
"@metamask/base-controller": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>@metamask/base-controller": true,
"@metamask/smart-transactions-controller>@metamask/controller-utils": true,
"@metamask/smart-transactions-controller>bignumber.js": true,
"@metamask/smart-transactions-controller>isomorphic-fetch": true,
"fast-json-patch": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/smart-transactions-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
@ -1694,7 +1574,7 @@
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1713,6 +1593,25 @@
"define": true
}
},
"@metamask/smart-transactions-controller>isomorphic-fetch": {
"globals": {
"fetch.bind": true
},
"packages": {
"@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": true
}
},
"@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": {
"globals": {
"Blob": true,
"FileReader": true,
"FormData": true,
"URLSearchParams.prototype.isPrototypeOf": true,
"XMLHttpRequest": true,
"define": true,
"setTimeout": true
}
},
"@metamask/snaps-controllers": {
"globals": {
"URL": true,
@ -1724,12 +1623,11 @@
"setTimeout": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/permission-controller": true,
"@metamask/post-message-stream": true,
"@metamask/providers>@metamask/object-multiplex": true,
"@metamask/rpc-methods": true,
"@metamask/snaps-controllers>@metamask/base-controller": true,
"@metamask/snaps-controllers>@metamask/subject-metadata-controller": true,
"@metamask/snaps-controllers>@xstate/fsm": true,
"@metamask/snaps-controllers>concat-stream": true,
"@metamask/snaps-controllers>gunzip-maybe": true,
@ -1738,6 +1636,7 @@
"@metamask/snaps-controllers>tar-stream": true,
"@metamask/snaps-utils": true,
"@metamask/snaps-utils>@metamask/snaps-registry": true,
"@metamask/subject-metadata-controller": true,
"@metamask/utils": true,
"eth-rpc-errors": true,
"json-rpc-engine": true,
@ -1745,16 +1644,6 @@
"pump": true
}
},
"@metamask/snaps-controllers>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/snaps-controllers>@metamask/subject-metadata-controller": {
"packages": {
"@metamask/snaps-controllers>@metamask/base-controller": true
}
},
"@metamask/snaps-controllers>concat-stream": {
"packages": {
"@metamask/snaps-controllers>concat-stream>readable-stream": true,
@ -2988,7 +2877,7 @@
},
"browserify>has": {
"packages": {
"mocha>object.assign>function-bind": true
"browserify>has>function-bind": true
}
},
"browserify>os-browserify": {
@ -4043,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": {
@ -4457,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
}
@ -4795,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
}
},
@ -4807,7 +4696,7 @@
},
"packages": {
"browserify>has": true,
"mocha>object.assign>function-bind": true,
"browserify>has>function-bind": true,
"string.prototype.matchall>has-symbols": true
}
},

View File

@ -660,28 +660,8 @@
},
"@metamask/address-book-controller": {
"packages": {
"@metamask/address-book-controller>@metamask/base-controller": true,
"@metamask/address-book-controller>@metamask/controller-utils": true
}
},
"@metamask/address-book-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/address-book-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
"@metamask/base-controller": true,
"@metamask/controller-utils": true
}
},
"@metamask/announcement-controller": {
@ -716,12 +696,12 @@
"@ethersproject/contracts": true,
"@ethersproject/providers": true,
"@metamask/assets-controllers>@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/controller-utils": true,
"@metamask/assets-controllers>@metamask/utils": true,
"@metamask/assets-controllers>abort-controller": true,
"@metamask/assets-controllers>multiformats": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/metamask-eth-abis": true,
"browserify>events": true,
"eth-json-rpc-filters>async-mutex": true,
@ -750,21 +730,6 @@
"semver": true
}
},
"@metamask/assets-controllers>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/assets-controllers>@metamask/utils": {
"globals": {
"TextDecoder": true,
@ -1089,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
}
},
@ -1193,34 +1158,14 @@
"setInterval": true
},
"packages": {
"@metamask/gas-fee-controller>@metamask/base-controller": true,
"@metamask/gas-fee-controller>@metamask/controller-utils": true,
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"eth-query": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true,
"uuid": true
}
},
"@metamask/gas-fee-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/gas-fee-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/jazzicon": {
"globals": {
"document.createElement": true,
@ -1319,8 +1264,8 @@
},
"@metamask/message-manager": {
"packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/message-manager>@metamask/base-controller": true,
"@metamask/message-manager>jsonschema": true,
"browserify>buffer": true,
"browserify>events": true,
@ -1329,11 +1274,6 @@
"uuid": true
}
},
"@metamask/message-manager>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/message-manager>jsonschema": {
"packages": {
"browserify>url": true
@ -1364,8 +1304,8 @@
"console.error": true
},
"packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/permission-controller>nanoid": true,
"deep-freeze-strict": true,
"eth-rpc-errors": true,
@ -1373,11 +1313,6 @@
"json-rpc-engine": true
}
},
"@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/permission-controller>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1389,46 +1324,11 @@
},
"packages": {
"@metamask/base-controller": true,
"@metamask/phishing-controller>@metamask/controller-utils": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/controller-utils": true,
"@metamask/phishing-warning>eth-phishing-detect": true,
"punycode": true
}
},
"@metamask/phishing-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/phishing-controller>isomorphic-fetch": {
"globals": {
"fetch.bind": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": true
}
},
"@metamask/phishing-controller>isomorphic-fetch>whatwg-fetch": {
"globals": {
"Blob": true,
"FileReader": true,
"FormData": true,
"URLSearchParams.prototype.isPrototypeOf": true,
"XMLHttpRequest": true,
"define": true,
"setTimeout": true
}
},
"@metamask/phishing-warning>eth-phishing-detect": {
"packages": {
"eslint>optionator>fast-levenshtein": true
@ -1491,14 +1391,19 @@
"@ethersproject/abi>@ethersproject/bytes": true,
"@ethersproject/bignumber": true,
"@ethersproject/providers": true,
"@metamask/base-controller": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>@metamask/base-controller": true,
"@metamask/smart-transactions-controller>@metamask/controller-utils": true,
"@metamask/smart-transactions-controller>bignumber.js": true,
"@metamask/smart-transactions-controller>isomorphic-fetch": true,
"fast-json-patch": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/smart-transactions-controller>@metamask/controller-utils": {
"globals": {
"console.error": true,
@ -1506,7 +1411,7 @@
"setTimeout": true
},
"packages": {
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -1525,6 +1430,25 @@
"define": true
}
},
"@metamask/smart-transactions-controller>isomorphic-fetch": {
"globals": {
"fetch.bind": true
},
"packages": {
"@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": true
}
},
"@metamask/smart-transactions-controller>isomorphic-fetch>whatwg-fetch": {
"globals": {
"Blob": true,
"FileReader": true,
"FormData": true,
"URLSearchParams.prototype.isPrototypeOf": true,
"XMLHttpRequest": true,
"define": true,
"setTimeout": true
}
},
"@metamask/snaps-controllers>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -2560,7 +2484,7 @@
},
"browserify>has": {
"packages": {
"mocha>object.assign>function-bind": true
"browserify>has>function-bind": true
}
},
"browserify>os-browserify": {
@ -3615,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": {
@ -4235,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
}
},
@ -4247,7 +4171,7 @@
},
"packages": {
"browserify>has": true,
"mocha>object.assign>function-bind": true,
"browserify>has>function-bind": true,
"string.prototype.matchall>has-symbols": true
}
},

View File

@ -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
}

View File

@ -226,11 +226,11 @@
"@keystonehq/metamask-airgapped-keyring": "^0.6.1",
"@lavamoat/snow": "^1.5.0",
"@material-ui/core": "^4.11.0",
"@metamask/address-book-controller": "^1.0.0",
"@metamask/announcement-controller": "^2.0.1",
"@metamask/approval-controller": "^1.0.0",
"@metamask/assets-controllers": "^4.0.1",
"@metamask/base-controller": "^1.0.0",
"@metamask/address-book-controller": "^2.0.0",
"@metamask/announcement-controller": "^3.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",
"@metamask/controller-utils": "^3.1.0",
"@metamask/design-tokens": "^1.9.0",
@ -242,19 +242,19 @@
"@metamask/eth-ledger-bridge-keyring": "^0.13.0",
"@metamask/eth-token-tracker": "^4.0.0",
"@metamask/etherscan-link": "^2.2.0",
"@metamask/gas-fee-controller": "^1.0.0",
"@metamask/gas-fee-controller": "^4.0.0",
"@metamask/jazzicon": "^2.0.0",
"@metamask/key-tree": "^7.0.0",
"@metamask/logo": "^3.1.1",
"@metamask/message-manager": "^2.1.0",
"@metamask/message-manager": "^3.0.0",
"@metamask/metamask-eth-abis": "^3.0.0",
"@metamask/notification-controller": "^1.0.0",
"@metamask/obs-store": "^8.0.0",
"@metamask/notification-controller": "^2.0.0",
"@metamask/obs-store": "^8.1.0",
"@metamask/permission-controller": "^3.1.0",
"@metamask/phishing-controller": "^2.0.0",
"@metamask/phishing-controller": "^3.0.0",
"@metamask/post-message-stream": "^6.0.0",
"@metamask/providers": "^10.2.1",
"@metamask/rate-limit-controller": "^1.0.0",
"@metamask/rate-limit-controller": "^2.0.0",
"@metamask/rpc-methods": "^0.32.2",
"@metamask/safe-event-emitter": "^2.0.0",
"@metamask/scure-bip39": "^2.0.3",
@ -263,7 +263,7 @@
"@metamask/snaps-controllers": "^0.32.2",
"@metamask/snaps-ui": "^0.32.2",
"@metamask/snaps-utils": "^0.32.2",
"@metamask/subject-metadata-controller": "^1.0.0",
"@metamask/subject-metadata-controller": "^2.0.0",
"@metamask/swappable-obj-proxy": "^2.1.0",
"@metamask/utils": "^5.0.0",
"@ngraveio/bc-ur": "^1.1.6",
@ -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",
@ -419,6 +420,7 @@
"@types/react-dom": "^17.0.11",
"@types/react-redux": "^7.1.25",
"@types/remote-redux-devtools": "^0.5.5",
"@types/sinon": "^10.0.13",
"@types/w3c-web-hid": "^1.0.3",
"@types/watchify": "^3.11.1",
"@types/yargs": "^17.0.8",
@ -451,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",
@ -495,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",

View File

@ -53,6 +53,7 @@ export const MESSAGE_TYPE = {
PERSONAL_SIGN: 'personal_sign',
SEND_METADATA: 'metamask_sendDomainMetadata',
SWITCH_ETHEREUM_CHAIN: 'wallet_switchEthereumChain',
TRANSACTION: 'transaction',
WALLET_REQUEST_PERMISSIONS: 'wallet_requestPermissions',
WATCH_ASSET: 'wallet_watchAsset',
WATCH_ASSET_LEGACY: 'metamask_watchAsset',

View File

@ -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',

View File

@ -238,7 +238,7 @@ export const INFURA_PROVIDER_TYPES = [
NETWORK_TYPES.MAINNET,
NETWORK_TYPES.GOERLI,
NETWORK_TYPES.SEPOLIA,
];
] as const;
export const TEST_CHAINS = [
CHAIN_IDS.GOERLI,

View File

@ -0,0 +1 @@
export const TERMS_OF_USE_LAST_UPDATED = '2023-03-25';

View File

@ -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"
}
}

View File

@ -1535,7 +1535,18 @@
"origin": "tmashuang.github.io"
}
],
"desktopEnabled": false
"desktopEnabled": false,
"pendingApprovals": {
"testApprovalId": {
"id": "testApprovalId",
"time": 1528133319641,
"origin": "metamask",
"type": "transaction",
"requestData": { "txId": "testTransactionId" },
"requestState": { "test": "value" }
}
},
"pendingApprovalCount": 1
},
"send": {
"amountMode": "INPUT",

View File

@ -140,6 +140,7 @@ function defaultFixture() {
browserEnvironment: {},
nftsDropdownState: {},
connectedStatusPopoverHasBeenShown: true,
termsOfUseLastAgreed: 86400000000000,
defaultHomeActiveTabName: null,
fullScreenGasPollTokens: [],
notificationGasPollTokens: [],

View File

@ -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"]');

View File

@ -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"]');
});

View File

@ -62,9 +62,7 @@ describe('ERC721 NFTs testdapp interaction', function () {
);
// Verify transaction
const completedTx = await driver.findElement('.list-item__title');
const completedTxText = await completedTx.getText();
assert.equal(completedTxText, 'Send Token');
await driver.findElement({ text: 'Send TDC' });
},
);
});

View File

@ -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',

View File

@ -119,16 +119,16 @@ describe('Create token, approve token and approve token without gas', function (
);
await driver.clickElement({
text: 'Verify contract details',
text: 'Verify third-party details',
css: '.token-allowance-container__verify-link',
});
const modalTitle = await driver.waitForSelector({
text: 'Contract details',
text: 'Third-party details',
tag: 'h5',
});
assert.equal(await modalTitle.getText(), 'Contract details');
assert.equal(await modalTitle.getText(), 'Third-party details');
await driver.clickElement({
text: 'Got it',
@ -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({

View File

@ -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"]');

View File

@ -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"]');

View File

@ -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

View File

@ -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');
},
);

View File

@ -60,7 +60,7 @@ describe('Sign Typed Data V4 Signature Request', function () {
assert.equal(await origin.getText(), 'http://127.0.0.1:8080');
verifyContractDetailsButton.click();
await driver.findElement({ text: 'Contract details', tag: 'h5' });
await driver.findElement({ text: 'Third-party details', tag: 'h5' });
await driver.findElement('[data-testid="recipient"]');
await driver.clickElement({ text: 'Got it', tag: 'button' });
@ -142,7 +142,7 @@ describe('Sign Typed Data V3 Signature Request', function () {
assert.equal(await origin.getText(), 'http://127.0.0.1:8080');
verifyContractDetailsButton.click();
await driver.findElement({ text: 'Contract details', tag: 'h5' });
await driver.findElement({ text: 'Third-party details', tag: 'h5' });
await driver.findElement('[data-testid="recipient"]');
await driver.clickElement({ text: 'Got it', tag: 'button' });

View File

@ -5,5 +5,11 @@ declare module '@metamask/eth-keyring-controller' {
signPersonalMessage: (...any) => any;
signTypedMessage: (...any) => any;
getKeyringForAccount: (address: string) => Promise<{
type: string;
}>;
getEncryptionPublicKey: (address: string) => Promise<string>;
}
}

50
types/eth-query.d.ts vendored Normal file
View File

@ -0,0 +1,50 @@
declare module 'eth-query' {
// What it says on the tin. We omit `null` because confusingly, this is used
// for a successful response to indicate a lack of an error.
type EverythingButNull =
| string
| number
| boolean
| object
| symbol
| undefined;
type ProviderSendAsyncResponse<Result> = {
error?: { message: string };
result?: Result;
};
type ProviderSendAsyncCallback<Result> = (
error: unknown,
response: ProviderSendAsyncResponse<Result>,
) => void;
type Provider = {
sendAsync<Params, Result>(
payload: SendAsyncPayload<Params>,
callback: ProviderSendAsyncCallback<Result>,
): void;
};
type SendAsyncPayload<Params> = {
id: number;
jsonrpc: '2.0';
method: string;
params: Params;
};
type SendAsyncCallback<Result> = (
...args:
| [error: EverythingButNull, result: undefined]
| [error: null, result: Result]
) => void;
export default class EthQuery {
constructor(provider: Provider);
sendAsync<Params, Result>(
opts: Partial<SendAsyncPayload<Params>>,
callback: SendAsyncCallback<Result>,
): void;
}
}

View File

@ -82,6 +82,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';

View File

@ -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 &&

View File

@ -14,7 +14,7 @@ import {
import { BETA_BUGS_URL } from '../../../helpers/constants/beta';
import { hideBetaHeader } from '../../../store/actions';
import { ButtonIcon } from '../../component-library';
import { ButtonIcon } from '../../component-library/button-icon/deprecated';
import {
ICON_NAMES,
ICON_SIZES,

View File

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { useRef } from 'react';
import { Menu } from '../../../ui/menu';
import { ICON_NAMES } from '../../../component-library/icon/deprecated';
import { ButtonIcon } from '../../../component-library';
import { ButtonIcon } from '../../../component-library/button-icon/deprecated';
import { useI18nContext } from '../../../../hooks/useI18nContext';
const ConnectedAccountsListOptions = ({

View File

@ -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({
}
>
<FormField
inputRef={inputRef}
dataTestId="custom-spending-cap-input"
autoFocus
wrappingLabelProps={{ as: 'div' }}
id={
decConversionGreaterThan(value, currentTokenBalance)

View File

@ -20,7 +20,8 @@ import {
getSnapName,
removeSnapIdPrefix,
} from '../../../../helpers/utils/util';
import { Text, ButtonIcon } from '../../../component-library';
import { ButtonIcon } from '../../../component-library/button-icon/deprecated';
import { Text } from '../../../component-library';
import {
ICON_NAMES,
ICON_SIZES,

View File

@ -0,0 +1 @@
export { default } from './snap-permissions-list';

View File

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getWeightedPermissions } from '../../../../helpers/utils/permission';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import PermissionCell from '../../permission-cell';
import Box from '../../../ui/box';
export default function SnapPermissionsList({
permissions,
targetSubjectMetadata,
}) {
const t = useI18nContext();
return (
<Box paddingTop={2} paddingBottom={2} className="snap-permissions-list">
{getWeightedPermissions(t, permissions, targetSubjectMetadata).map(
(permission, index) => {
return (
<PermissionCell
title={permission.label}
description={permission.description}
weight={permission.weight}
avatarIcon={permission.leftIcon}
dateApproved={permission?.permissionValue?.date}
key={`${permission.permissionName}-${index}`}
/>
);
},
)}
</Box>
);
}
SnapPermissionsList.propTypes = {
permissions: PropTypes.object.isRequired,
targetSubjectMetadata: PropTypes.object.isRequired,
};

View File

@ -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) => <SnapPermissionsList {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
permissions: {
eth_accounts: {},
snap_dialog: {},
snap_getBip32PublicKey: {
caveats: [
{
value: [
{
path: ['m', `44'`, `0'`],
curve: 'secp256k1',
},
],
},
],
},
},
};

View File

@ -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(
<SnapPermissionsList
permissions={mockPermissionData}
targetSubjectMetadata={mockTargetSubjectMetadata}
/>,
);
expect(
screen.getByText('Display dialog windows in MetaMask.'),
).toBeInTheDocument();
expect(screen.getByText('Approved on 2023-04-05')).toBeInTheDocument();
});
});

View File

@ -1,6 +1,5 @@
import React from 'react';
import { Provider } from 'react-redux';
import { object } from '@storybook/addon-knobs';
import { panel, text, heading, divider, copyable } from '@metamask/snaps-ui';
import configureStore from '../../../../store/store';
import testData from '../../../../../.storybook/test-data';
@ -10,8 +9,13 @@ const store = configureStore(testData);
export default {
title: 'Components/App/SnapUIRenderer',
component: SnapUIRenderer,
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
argTypes: {
data: {
control: 'object',
},
},
};
const DATA = panel([
@ -22,13 +26,18 @@ const DATA = panel([
copyable('Text you can copy'),
]);
export const DefaultStory = () => (
<SnapUIRenderer
snapId="local:http://localhost:8080/"
data={object('data', DATA)}
/>
export const DefaultStory = (args) => (
<SnapUIRenderer snapId="local:http://localhost:8080/" data={args.data} />
);
export const ErrorStory = () => (
<SnapUIRenderer snapId="local:http://localhost:8080/" data="foo" />
DefaultStory.args = {
data: DATA,
};
export const ErrorStory = (args) => (
<SnapUIRenderer snapId="local:http://localhost:8080/" data={args.data} />
);
ErrorStory.args = {
data: 'foo',
};

View File

@ -170,8 +170,9 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) {
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
className="hold-to-reveal-button__button-hold"
textProps={{ display: DISPLAY.FLEX, alignItems: AlignItems.center }}
>
<Box className="hold-to-reveal-button__icon-container">
<Box className="hold-to-reveal-button__icon-container" marginRight={2}>
{renderPreCompleteContent()}
{renderPostCompleteContent()}
</Box>

View File

@ -14,9 +14,8 @@ import { CONNECTED_ACCOUNTS_ROUTE } from '../../../helpers/constants/routes';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { getOriginOfCurrentTab } from '../../../selectors';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { ButtonIcon } from '../../component-library';
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 (
<div className="menu-bar">
{showStatus ? ( // TODO: Move the connection status menu icon to the correct position in header once we implement the new header
{showStatus ? (
<ConnectedStatusIndicator
onClick={() => history.push(CONNECTED_ACCOUNTS_ROUTE)}
/>
@ -58,18 +57,12 @@ export default function MenuBar() {
}}
/>
</span>
{accountOptionsMenuOpen &&
(process.env.MULTICHAIN ? (
<GlobalMenu
anchorElement={ref.current}
closeMenu={() => setAccountOptionsMenuOpen(false)}
/>
) : (
<AccountOptionsMenu
anchorElement={ref.current}
onClose={() => setAccountOptionsMenuOpen(false)}
/>
))}
{accountOptionsMenuOpen && (
<AccountOptionsMenu
anchorElement={ref.current}
onClose={() => setAccountOptionsMenuOpen(false)}
/>
)}
</div>
);
}

View File

@ -25,7 +25,7 @@ import { useCopyToClipboard } from '../../../../hooks/useCopyToClipboard';
import { getAddressBookEntry } from '../../../../selectors';
import { TokenStandard } from '../../../../../shared/constants/transaction';
import NftCollectionImage from '../../../ui/nft-collection-image/nft-collection-image';
import { ButtonIcon } from '../../../component-library';
import { ButtonIcon } from '../../../component-library/button-icon/deprecated';
import { ICON_NAMES } from '../../../component-library/icon/deprecated';
export default function ContractDetailsModal({

View File

@ -15,7 +15,7 @@ import Box from '../../../ui/box';
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url';
import { ButtonIcon } from '../../../component-library';
import { ButtonIcon } from '../../../component-library/button-icon/deprecated';
import {
ICON_NAMES,
ICON_SIZES,

View File

@ -10,7 +10,7 @@ import {
calcTokenAmount,
toPrecisionWithoutTrailingZeros,
} from '../../../../../shared/lib/transactions-controller-utils';
import { ButtonIcon } from '../../../component-library';
import { ButtonIcon } from '../../../component-library/button-icon/deprecated';
import {
ICON_SIZES,
ICON_NAMES,

View File

@ -2,12 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react';
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
import Box from '../../../ui/box';
import {
Text,
Button,
BUTTON_TYPES,
ButtonIcon,
} from '../../../component-library';
import { Text, Button, BUTTON_TYPES } from '../../../component-library';
import { ButtonIcon } from '../../../component-library/button-icon/deprecated';
import { ICON_NAMES } from '../../../component-library/icon/deprecated';
import {
AlignItems,

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '../../../ui/button/button.component';
import { ButtonIcon } from '../../../component-library';
import { ButtonIcon } from '../../../component-library/button-icon/deprecated';
import { ICON_NAMES } from '../../../component-library/icon/deprecated';
export default class NewAccountModal extends Component {

View File

@ -53,7 +53,7 @@ import {
TokenStandard,
} from '../../../../shared/constants/transaction';
import NftDefaultImage from '../nft-default-image';
import { ButtonIcon } from '../../component-library';
import { ButtonIcon } from '../../component-library/button-icon/deprecated';
import { ICON_NAMES } from '../../component-library/icon/deprecated';
import Tooltip from '../../ui/tooltip';
import { decWEIToDecETH } from '../../../../shared/modules/conversion.utils';

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { I18nContext } from '../../../contexts/i18n';
import { Menu, MenuItem } from '../../ui/menu';
import { ButtonIcon } from '../../component-library';
import { ButtonIcon } from '../../component-library/button-icon/deprecated';
import { ICON_NAMES } from '../../component-library/icon/deprecated';
import { Color } from '../../../helpers/constants/design-system';

View File

@ -53,6 +53,11 @@ const PermissionCell = ({
iconBackgroundColor = Color.backgroundAlternative;
}
let permissionIcon = avatarIcon;
if (typeof avatarIcon !== 'string' && avatarIcon?.props?.iconName) {
permissionIcon = avatarIcon.props.iconName;
}
return (
<Box
className="permission-cell"
@ -64,9 +69,9 @@ const PermissionCell = ({
paddingBottom={2}
>
<Box>
{typeof avatarIcon === 'string' ? (
{typeof permissionIcon === 'string' ? (
<AvatarIcon
iconName={avatarIcon}
iconName={permissionIcon}
size={ICON_SIZES.MD}
iconProps={{
size: ICON_SIZES.SM,
@ -75,7 +80,7 @@ const PermissionCell = ({
backgroundColor={iconBackgroundColor}
/>
) : (
avatarIcon
permissionIcon
)}
</Box>
<Box width="full" marginLeft={4} marginRight={4}>

View File

@ -28,11 +28,14 @@ export default class PermissionPageContainerContent extends PureComponent {
};
renderRequestedPermissions() {
const { selectedPermissions } = this.props;
const { selectedPermissions, subjectMetadata } = this.props;
return (
<div className="permission-approval-container__content__requested">
<PermissionsConnectPermissionList permissions={selectedPermissions} />
<PermissionsConnectPermissionList
permissions={selectedPermissions}
targetSubjectMetadata={subjectMetadata}
/>
</div>
);
}

View File

@ -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 (
<div className="permission" key={`${permissionName}-${index}`}>
{typeof leftIcon === 'string' ? <i className={leftIcon} /> : leftIcon}
{label}
{getRightIcon(permission)}
</div>
);
}
export default function PermissionsConnectPermissionList({
permissions,
@ -12,22 +32,11 @@ export default function PermissionsConnectPermissionList({
const t = useI18nContext();
return (
<Box paddingTop={2} paddingBottom={2}>
<div className="permissions-connect-permission-list">
{getWeightedPermissions(t, permissions, targetSubjectMetadata).map(
(permission, index) => {
return (
<PermissionCell
title={permission.label}
description={permission.description}
weight={permission.weight}
avatarIcon={permission.leftIcon}
dateApproved={permission?.permissionValue?.date}
key={`${permission.permissionName}-${index}`}
/>
);
},
getDescriptionNode,
)}
</Box>
</div>
);
}

View File

@ -20,18 +20,5 @@ DefaultStory.storyName = 'Default';
DefaultStory.args = {
permissions: {
eth_accounts: {},
snap_dialog: {},
snap_getBip32PublicKey: {
caveats: [
{
value: [
{
path: ['m', `44'`, `0'`],
curve: 'secp256k1',
},
],
},
],
},
},
};

View File

@ -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({
])}
</BannerAlert>
)}
{isLedgerWallet && (
<div className="confirm-approve-content__ledger-instruction-wrapper">
<LedgerInstructionField showDataInstruction />
</div>
)}
{!isSIWEDomainValid && (
<BannerAlert
severity={SEVERITIES.DANGER}

View File

@ -52,6 +52,15 @@ const mockProps = {
},
};
jest.mock('../ledger-instruction-field', () => {
return {
__esModule: true,
default: () => {
return <div className="mock-ledger-instruction-field" />;
},
};
});
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();
});
});

View File

@ -224,7 +224,7 @@ exports[`Signature Request Component render should match snapshot when we are us
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h7 typography--weight-normal typography--style-normal typography--color-primary-default"
>
Verify contract details
Verify third-party details
</h6>
</a>
</div>
@ -999,7 +999,7 @@ exports[`Signature Request Component render should match snapshot when we want t
<h6
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h7 typography--weight-normal typography--style-normal typography--color-primary-default"
>
Verify contract details
Verify third-party details
</h6>
</a>
</div>

View File

@ -0,0 +1 @@
export { default } from './terms-of-use-popup';

View File

@ -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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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) => <TermsOfUsePopup {...args} />;
DefaultStory.storyName = 'Default';

View File

@ -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(<TermsOfUsePopup onAccept={onAccept} />, 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',
});
});
});

View File

@ -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 (
<div className={classnames('wallet-overview', className)}>
<div className="wallet-overview__balance">
{loading ? null : icon}
{process.env.MULTICHAIN ? (
<AddressCopyButton address={checksummedAddress} shorten />
) : (
<>{loading ? null : icon}</>
)}
{balance}
</div>
<div className="wallet-overview__buttons">{buttons}</div>

View File

@ -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 {

View File

@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { ButtonIcon, ButtonLink, Text } from '..';
import { IconName } from '../icon';
import { ButtonIcon } from '../button-icon/deprecated';
import { ButtonLink, Text } from '..';
import { ICON_NAMES } from '../icon/deprecated';
import Box from '../../ui/box';
@ -72,7 +73,7 @@ export const BannerBase = ({
<ButtonIcon
className="mm-banner-base__close-button"
marginLeft="auto"
iconName={IconName.Close}
iconName={ICON_NAMES.CLOSE}
size={Size.SM}
ariaLabel="Close" // TODO: i18n
onClick={onClose}

View File

@ -34,26 +34,23 @@ import { IconName } from '../icon';
### Size
Use the `size` prop and the `Size` object from `./ui/helpers/constants/design-system.js`
to change the size of `ButtonIcon`. Defaults to `Size.SM`
Optional: `BUTTON_ICON_SIZES` from `./button-icon` object can be used instead of `Size`.
Use the `size` prop and the `ButtonIconSize` enum from `./ui/components/component-library/icon` to change the size of `ButtonIcon`. Defaults to `ButtonIconSize.Sm`
Possible sizes include:
- `Size.SM` 24px
- `Size.LG` 32px
- `ButtonIconSize.Sm` 24px
- `ButtonIconSize.Lg` 32px
<Canvas>
<Story id="components-componentlibrary-buttonicon--size-story" />
</Canvas>
```jsx
import { Size } from '../../../helpers/constants/design-system';
import { ButtonIconSize } from '../../../helpers/constants/design-system';
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon size={Size.SM} iconName={IconName.Close} ariaLabel="Close"/>
<ButtonIcon size={Size.LG} iconName={IconName.Close} ariaLabel="Close"/>
<ButtonIcon size={ButtonIconSize.Sm} iconName={IconName.Close} ariaLabel="Close"/>
<ButtonIcon size={ButtonIconSize.Lg} iconName={IconName.Close} ariaLabel="Close"/>
```
### Aria Label

View File

@ -1,35 +1,11 @@
import React from 'react';
import {
AlignItems,
Color,
DISPLAY,
FLEX_DIRECTION,
Size,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box/box';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { Color } from '../../../helpers/constants/design-system';
import { IconName } from '..';
import { BUTTON_ICON_SIZES } from './button-icon.constants';
import { ButtonIconSize } from './button-icon.types';
import { ButtonIcon } from './button-icon';
import README from './README.mdx';
const marginSizeControlOptions = [
undefined,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
'auto',
];
export default {
title: 'Components/ComponentLibrary/ButtonIcon',
@ -40,58 +16,18 @@ export default {
},
},
argTypes: {
ariaLabel: {
control: 'text',
},
as: {
control: 'select',
options: ['button', 'a'],
},
className: {
control: 'text',
},
color: {
control: 'select',
options: Object.values(Color),
},
disabled: {
control: 'boolean',
},
href: {
control: 'text',
},
iconName: {
control: 'select',
options: Object.values(IconName),
},
size: {
control: 'select',
options: Object.values(BUTTON_ICON_SIZES),
},
marginTop: {
options: marginSizeControlOptions,
control: 'select',
table: { category: 'box props' },
},
marginRight: {
options: marginSizeControlOptions,
control: 'select',
table: { category: 'box props' },
},
marginBottom: {
options: marginSizeControlOptions,
control: 'select',
table: { category: 'box props' },
},
marginLeft: {
options: marginSizeControlOptions,
control: 'select',
table: { category: 'box props' },
},
},
};
} as ComponentMeta<typeof ButtonIcon>;
export const DefaultStory = (args) => <ButtonIcon {...args} />;
const Template: ComponentStory<typeof ButtonIcon> = (args) => (
<ButtonIcon {...args} />
);
export const DefaultStory = Template.bind({});
DefaultStory.args = {
iconName: IconName.Close,
@ -100,7 +36,9 @@ DefaultStory.args = {
DefaultStory.storyName = 'Default';
export const IconNameStory = (args) => <ButtonIcon {...args} />;
export const IconNameStory: ComponentStory<typeof ButtonIcon> = (args) => (
<ButtonIcon {...args} />
);
IconNameStory.args = {
iconName: IconName.Close,
@ -109,32 +47,27 @@ IconNameStory.args = {
IconNameStory.storyName = 'IconName';
export const SizeStory = (args) => (
<Box
display={DISPLAY.FLEX}
alignItems={AlignItems.baseline}
gap={1}
marginBottom={2}
>
export const SizeStory: ComponentStory<typeof ButtonIcon> = (args) => (
<>
<ButtonIcon
{...args}
size={Size.SM}
size={ButtonIconSize.Sm}
iconName={IconName.Close}
ariaLabel="Close"
/>
<ButtonIcon
{...args}
size={Size.LG}
size={ButtonIconSize.Lg}
color={Color.primaryDefault}
iconName={IconName.Close}
ariaLabel="Close"
/>
</Box>
</>
);
SizeStory.storyName = 'Size';
export const AriaLabel = (args) => (
export const AriaLabel: ComponentStory<typeof ButtonIcon> = (args) => (
<>
<ButtonIcon
as="button"
@ -154,8 +87,8 @@ export const AriaLabel = (args) => (
</>
);
export const As = (args) => (
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW} gap={2}>
export const As: ComponentStory<typeof ButtonIcon> = (args) => (
<>
<ButtonIcon {...args} iconName={IconName.Close} ariaLabel="close" />
<ButtonIcon
as="a"
@ -165,10 +98,10 @@ export const As = (args) => (
iconName={IconName.Export}
ariaLabel="demo"
/>
</Box>
</>
);
export const Href = (args) => (
export const Href: ComponentStory<typeof ButtonIcon> = (args) => (
<ButtonIcon iconName={IconName.Export} {...args} target="_blank" />
);
@ -178,7 +111,7 @@ Href.args = {
color: Color.primaryDefault,
};
export const ColorStory = (args) => (
export const ColorStory: ComponentStory<typeof ButtonIcon> = (args) => (
<ButtonIcon {...args} iconName={IconName.Close} ariaLabel="close" />
);
ColorStory.storyName = 'Color';
@ -187,7 +120,7 @@ ColorStory.args = {
color: Color.primaryDefault,
};
export const Disabled = (args) => (
export const Disabled: ComponentStory<typeof ButtonIcon> = (args) => (
<ButtonIcon {...args} iconName={IconName.Close} ariaLabel="close" />
);

View File

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

View File

@ -0,0 +1,72 @@
import React from 'react';
import classnames from 'classnames';
import {
AlignItems,
BackgroundColor,
BorderRadius,
DISPLAY,
IconColor,
JustifyContent,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box';
import { Icon, IconSize } from '../icon';
import { ButtonIconSize, ButtonIconProps } from './button-icon.types';
const buttonIconSizeToIconSize: Record<ButtonIconSize, IconSize> = {
[ButtonIconSize.Sm]: IconSize.Sm,
[ButtonIconSize.Lg]: IconSize.Lg,
};
export const ButtonIcon = React.forwardRef(
(
{
ariaLabel,
as = 'button',
className = '',
color = IconColor.iconDefault,
href,
size = ButtonIconSize.Lg,
iconName,
disabled,
iconProps,
...props
}: ButtonIconProps,
ref: React.Ref<HTMLElement>,
) => {
const Tag = href ? 'a' : as;
const isDisabled = disabled && Tag === 'button';
return (
<Box
aria-label={ariaLabel}
as={Tag}
className={classnames(
'mm-button-icon',
`mm-button-icon--size-${String(size)}`,
{
'mm-button-icon--disabled': Boolean(disabled),
},
className,
)}
color={color}
{...(isDisabled ? { disabled: true } : {})} // only allow disabled attribute to be passed down to the Box when the as prop is equal to a button element
display={DISPLAY.INLINE_FLEX}
justifyContent={JustifyContent.center}
alignItems={AlignItems.center}
borderRadius={BorderRadius.LG}
backgroundColor={BackgroundColor.transparent}
{...(href ? { href } : {})}
ref={ref}
{...props}
>
<Icon
name={iconName}
size={buttonIconSizeToIconSize[size]}
{...iconProps}
/>
</Box>
);
},
);

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