1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 18:00:18 +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', '**/__snapshots__/*.snap',
'app/scripts/controllers/app-state.test.js', 'app/scripts/controllers/app-state.test.js',
'app/scripts/controllers/network/**/*.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/controllers/permissions/**/*.test.js',
'app/scripts/lib/**/*.test.js', 'app/scripts/lib/**/*.test.js',
'app/scripts/migrations/*.test.js', 'app/scripts/migrations/*.test.js',

View File

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

View File

@ -109,6 +109,9 @@
"about": { "about": {
"message": "About" "message": "About"
}, },
"accept": {
"message": "Accept"
},
"acceptTermsOfUse": { "acceptTermsOfUse": {
"message": "I have read and agree to the $1", "message": "I have read and agree to the $1",
"description": "$1 is the `terms` message" "description": "$1 is the `terms` message"
@ -305,6 +308,10 @@
"advancedPriorityFeeToolTip": { "advancedPriorityFeeToolTip": {
"message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction." "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": { "airgapVault": {
"message": "AirGap Vault" "message": "AirGap Vault"
}, },
@ -815,7 +822,7 @@
"message": "Contract deployment" "message": "Contract deployment"
}, },
"contractDescription": { "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": { "contractInteraction": {
"message": "Contract interaction" "message": "Contract interaction"
@ -830,10 +837,10 @@
"message": "Contract requesting signature" "message": "Contract requesting signature"
}, },
"contractRequestingSpendingCap": { "contractRequestingSpendingCap": {
"message": "Contract requesting spending cap" "message": "Third party requesting spending cap"
}, },
"contractTitle": { "contractTitle": {
"message": "Contract details" "message": "Third-party details"
}, },
"contractToken": { "contractToken": {
"message": "Token contract" "message": "Token contract"
@ -929,6 +936,21 @@
"custodianAccount": { "custodianAccount": {
"message": "Custodian account" "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": { "custom": {
"message": "Advanced" "message": "Advanced"
}, },
@ -1808,14 +1830,14 @@
"message": "Your initial transaction was confirmed by the network. Click OK to go back." "message": "Your initial transaction was confirmed by the network. Click OK to go back."
}, },
"inputLogicEmptyState": { "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": { "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" "description": "$1 is the current token balance in the account and the name of the current token"
}, },
"inputLogicHigherNumber": { "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": { "insightsFromSnap": {
"message": "Insights from $1", "message": "Insights from $1",
@ -2486,6 +2508,9 @@
"message": "OpenSea is the first provider for this feature. More providers coming soon!", "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." "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": { "notifications19ActionText": {
"message": "Enable NFT autodetection" "message": "Enable NFT autodetection"
}, },
@ -3328,7 +3353,7 @@
"description": "$1 is a token symbol" "description": "$1 is a token symbol"
}, },
"revokeSpendingCapTooltipText": { "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": { "rpcUrl": {
"message": "New RPC URL" "message": "New RPC URL"
@ -4292,6 +4317,15 @@
"termsOfUse": { "termsOfUse": {
"message": "terms of use" "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": { "testNetworks": {
"message": "Test networks" "message": "Test networks"
}, },
@ -4660,7 +4694,7 @@
"message": "Username" "message": "Username"
}, },
"verifyContractDetails": { "verifyContractDetails": {
"message": "Verify contract details" "message": "Verify third-party details"
}, },
"verifyThisTokenDecimalOn": { "verifyThisTokenDecimalOn": {
"message": "Token decimal can be found on $1", "message": "Token decimal can be found on $1",
@ -4746,7 +4780,7 @@
"message": "Warning" "message": "Warning"
}, },
"warningTooltipText": { "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" "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour"
}, },
"weak": { "weak": {

View File

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

View File

@ -1,5 +1,7 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import { ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
import { v4 as uuid } from 'uuid';
import log from 'loglevel';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import { MINUTE } from '../../../shared/constants/time'; import { MINUTE } from '../../../shared/constants/time';
import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms'; import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms';
@ -8,8 +10,11 @@ import { isBeta } from '../../../ui/helpers/utils/build-types';
import { import {
ENVIRONMENT_TYPE_BACKGROUND, ENVIRONMENT_TYPE_BACKGROUND,
POLLING_TOKEN_ENVIRONMENT_TYPES, POLLING_TOKEN_ENVIRONMENT_TYPES,
ORIGIN_METAMASK,
} from '../../../shared/constants/app'; } from '../../../shared/constants/app';
const APPROVAL_REQUEST_TYPE = 'unlock';
export default class AppStateController extends EventEmitter { export default class AppStateController extends EventEmitter {
/** /**
* @param {object} opts * @param {object} opts
@ -20,9 +25,9 @@ export default class AppStateController extends EventEmitter {
isUnlocked, isUnlocked,
initState, initState,
onInactiveTimeout, onInactiveTimeout,
showUnlockRequest,
preferencesStore, preferencesStore,
qrHardwareStore, qrHardwareStore,
messenger,
} = opts; } = opts;
super(); super();
@ -59,8 +64,6 @@ export default class AppStateController extends EventEmitter {
this.waitingForUnlock = []; this.waitingForUnlock = [];
addUnlockListener(this.handleUnlock.bind(this)); addUnlockListener(this.handleUnlock.bind(this));
this._showUnlockRequest = showUnlockRequest;
preferencesStore.subscribe(({ preferences }) => { preferencesStore.subscribe(({ preferences }) => {
const currentState = this.store.getState(); const currentState = this.store.getState();
if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) { if (currentState.timeoutMinutes !== preferences.autoLockTimeLimit) {
@ -74,6 +77,9 @@ export default class AppStateController extends EventEmitter {
const { preferences } = preferencesStore.getState(); const { preferences } = preferencesStore.getState();
this._setInactiveTimeout(preferences.autoLockTimeLimit); 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.waitingForUnlock.push({ resolve });
this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE);
if (shouldShowUnlockRequest) { if (shouldShowUnlockRequest) {
this._showUnlockRequest(); this._requestApproval();
} }
} }
@ -122,6 +128,8 @@ export default class AppStateController extends EventEmitter {
} }
this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE); 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 * 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, 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'; import AppStateController from './app-state';
jest.mock('loglevel');
let appStateController, mockStore;
describe('AppStateController', () => { 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', () => { describe('setOutdatedBrowserWarningLastShown', () => {
it('should set the last shown time', () => { it('sets the last shown time', () => {
const appStateController = new AppStateController({ 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(), addUnlockListener: jest.fn(),
isUnlocked: jest.fn(() => true), isUnlocked: jest.fn(() => true),
initState: {},
onInactiveTimeout: jest.fn(), onInactiveTimeout: jest.fn(),
showUnlockRequest: jest.fn(), showUnlockRequest: jest.fn(),
preferencesStore: { preferencesStore: {
@ -20,14 +166,184 @@ describe('AppStateController', () => {
qrHardwareStore: { qrHardwareStore: {
subscribe: jest.fn(), 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( expect(
appStateController.store.getState().outdatedBrowserWarningLastShown, appStateController.store.getState().recoveryPhraseReminderLastShown,
).toStrictEqual(date); ).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 { NETWORK_TYPES } from '../../../shared/constants/network';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import DetectTokensController from './detect-tokens'; import DetectTokensController from './detect-tokens';
import NetworkController, { NetworkControllerEventTypes } from './network'; import { NetworkController, NetworkControllerEventType } from './network';
import PreferencesController from './preferences'; import PreferencesController from './preferences';
describe('DetectTokensController', function () { describe('DetectTokensController', function () {
@ -248,7 +248,7 @@ describe('DetectTokensController', function () {
), ),
onNetworkStateChange: (cb) => onNetworkStateChange: (cb) =>
networkControllerMessenger.subscribe( networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
() => { () => {
const networkState = network.store.getState(); const networkState = network.store.getState();
const modifiedNetworkState = { 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 */ /* 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 * 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"). * either `infura` or `custom` (default: "infura").
*/ */
export function testsForRpcMethodsThatCheckForBlockHashInResponse( export function testsForRpcMethodsThatCheckForBlockHashInResponse(
method, method: string,
{ numberOfParameters, providerType }, {
numberOfParameters,
providerType,
}: TestsForRpcMethodThatCheckForBlockHashInResponseOptions,
) { ) {
if (providerType !== 'infura' && providerType !== 'custom') { if (providerType !== 'infura' && providerType !== 'custom') {
throw new Error( throw new Error(

View File

@ -3,6 +3,7 @@
import { import {
buildMockParams, buildMockParams,
buildRequestWithReplacedBlockParam, buildRequestWithReplacedBlockParam,
ProviderType,
waitForPromiseToBeFulfilledAfterRunningAllTimers, waitForPromiseToBeFulfilledAfterRunningAllTimers,
withMockedCommunications, withMockedCommunications,
withNetworkClient, withNetworkClient,
@ -13,6 +14,12 @@ import {
buildJsonRpcEngineEmptyResponseErrorMessage, buildJsonRpcEngineEmptyResponseErrorMessage,
} from './shared-tests'; } from './shared-tests';
type TestsForRpcMethodSupportingBlockParam = {
providerType: ProviderType;
blockParamIndex: number;
numberOfParameters: number;
};
/** /**
* Defines tests which exercise the behavior exhibited by an RPC method that * 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 * 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 */ /* eslint-disable-next-line jest/no-export */
export function testsForRpcMethodSupportingBlockParam( export function testsForRpcMethodSupportingBlockParam(
method, method: string,
{ blockParamIndex, numberOfParameters, providerType }, {
blockParamIndex,
numberOfParameters,
providerType,
}: TestsForRpcMethodSupportingBlockParam,
) { ) {
describe.each([ describe.each([
['given no block tag', undefined], ['given no block tag', undefined],
@ -1718,9 +1729,9 @@ export function testsForRpcMethodSupportingBlockParam(
[ [
['less than the current block number', '0x200'], ['less than the current block number', '0x200'],
['equal to the curent block number', '0x100'], ['equal to the curent block number', '0x100'],
], ] as any,
'%s', '%s',
(_nestedDesc, currentBlockNumber) => { (_nestedDesc: string, currentBlockNumber: string) => {
it('makes an additional request to the RPC endpoint', async () => { it('makes an additional request to the RPC endpoint', async () => {
await withMockedCommunications({ providerType }, async (comms) => { await withMockedCommunications({ providerType }, async (comms) => {
const request = { const request = {

View File

@ -1,14 +1,13 @@
import nock from 'nock'; import nock, { Scope as NockScope } from 'nock';
import sinon from 'sinon'; import sinon from 'sinon';
import type { JSONRPCResponse } from '@json-rpc-specification/meta-schema';
import EthQuery from 'eth-query'; import EthQuery from 'eth-query';
import { createNetworkClient } from '../create-network-client'; import { Hex } from '@metamask/utils';
import { BuiltInInfuraNetwork } from '../../../../../shared/constants/network';
/** import {
* @typedef {import('nock').Scope} NockScope createNetworkClient,
* NetworkClientType,
* A object returned by the `nock` function for mocking requests to a particular } from '../create-network-client';
* base URL.
*/
/** /**
* A dummy value for the `infuraProjectId` option that `createInfuraClient` * 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 * keeps failing, you can set `process.env.DEBUG_PROVIDER_TESTS` to `1`. This
* will turn on some extra logging. * 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') { if (process.env.DEBUG_PROVIDER_TESTS === '1') {
console.log(...args); console.log(...args);
} }
@ -52,96 +51,89 @@ function debug(...args) {
/** /**
* Builds a Nock scope object for mocking provider requests. * Builds a Nock scope object for mocking provider requests.
* *
* @param {string} rpcUrl - The URL of the RPC endpoint. * @param rpcUrl - The URL of the RPC endpoint.
* @returns {NockScope} The nock scope. * @returns The nock scope.
*/ */
function buildScopeForMockingRequests(rpcUrl) { function buildScopeForMockingRequests(rpcUrl: string): NockScope {
return nock(rpcUrl).filteringRequestBody((body) => { return nock(rpcUrl).filteringRequestBody((body) => {
debug('Nock Received Request: ', body); debug('Nock Received Request: ', body);
return body; return body;
}); });
} }
/** type Request = { method: string; params?: any[] };
* @typedef {{ nockScope: NockScope, blockNumber: string }} MockBlockTrackerRequestOptions type Response = {
* id?: number | string;
* The options to `mockNextBlockTrackerRequest` and `mockAllBlockTrackerRequests`. jsonrpc?: '2.0';
*/ error?: any;
result?: any;
httpStatus?: number;
};
type ResponseBody = { body: JSONRPCResponse };
type BodyOrResponse = ResponseBody | Response;
type CurriedMockRpcCallOptions = {
request: Request;
// The response data.
response?: BodyOrResponse;
/**
* An error to throw while making the request.
* Takes precedence over `response`.
*/
error?: Error | string;
/**
* The amount of time that should pass before the
* request resolves with the response.
*/
delay?: number;
/**
* The number of times that the request is
* expected to be made.
*/
times?: number;
};
/** type MockRpcCallOptions = {
* Mocks the next request for the latest block that the block tracker will make. // A nock scope (a set of mocked requests scoped to a certain base URL).
* nockScope: nock.Scope;
* @param {MockBlockTrackerRequestOptions} args - The arguments. } & CurriedMockRpcCallOptions;
* @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 MockRpcCallResult = nock.Interceptor | nock.Scope;
* 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`.
*/
/** /**
* Mocks a JSON-RPC request sent to the provider with the given response. * 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. * Provider type is inferred from the base url set on the nockScope.
* *
* @param {MockRpcCallOptions} args - The arguments. * @param args - The arguments.
* @param {NockScope} args.nockScope - A nock scope (a set of mocked requests * @param args.nockScope - A nock scope (a set of mocked requests scoped to a
* scoped to a certain base URL). * certain base URL).
* @param {object} args.request - The request data. * @param args.request - The request data.
* @param {{body: string} | {httpStatus?: number; id?: number; method?: string; params?: string[]}} [args.response] - Information * @param args.response - Information concerning the response that the request
* concerning the response that the request should have. If a `body` property is * should have. If a `body` property is present, this is taken as the complete
* present, this is taken as the complete response body. If an `httpStatus` * response body. If an `httpStatus` property is present, then it is taken as
* property is present, then it is taken as the HTTP status code to respond * the HTTP status code to respond with. Properties other than these two are
* with. Properties other than these two are used to build a complete response * used to build a complete response body (including `id` and `jsonrpc`
* body (including `id` and `jsonrpc` properties). * properties).
* @param {Error | string} [args.error] - An error to throw while making the * @param args.error - An error to throw while making the request. Takes
* request. Takes precedence over `response`. * precedence over `response`.
* @param {number} [args.delay] - The amount of time that should pass before the * @param args.delay - The amount of time that should pass before the request
* request resolves with the response. * resolves with the response.
* @param {number} [args.times] - The number of times that the request is * @param args.times - The number of times that the request is expected to be
* expected to be made. * made.
* @returns {NockScope} The nock scope. * @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, // eth-query always passes `params`, so even if we don't supply this property,
// for consistency with makeRpcCall, assume that the `body` contains it // for consistency with makeRpcCall, assume that the `body` contains it
const { method, params = [], ...rest } = request; const { method, params = [], ...rest } = request;
let httpStatus = 200; let httpStatus = 200;
let completeResponse = { id: 2, jsonrpc: '2.0' }; let completeResponse: JSONRPCResponse = { id: 2, jsonrpc: '2.0' };
if (response !== undefined) { if (response !== undefined) {
if ('body' in response) { if ('body' in response) {
completeResponse = response.body; 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') const url = nockScope.basePath.includes('infura.io')
? `/v3/${MOCK_INFURA_PROJECT_ID}` ? `/v3/${MOCK_INFURA_PROJECT_ID}`
: '/'; : '/';
@ -189,7 +182,7 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) {
if (error !== undefined) { if (error !== undefined) {
return nockRequest.replyWithError(error); return nockRequest.replyWithError(error);
} else if (completeResponse !== undefined) { } else if (completeResponse !== undefined) {
return nockRequest.reply(httpStatus, (_, requestBody) => { return nockRequest.reply(httpStatus, (_, requestBody: any) => {
if (response !== undefined && !('body' in response)) { if (response !== undefined && !('body' in response)) {
if (response.id === undefined) { if (response.id === undefined) {
completeResponse.id = requestBody.id; completeResponse.id = requestBody.id;
@ -204,16 +197,72 @@ function mockRpcCall({ nockScope, request, response, error, delay, times }) {
return nockRequest; 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. * Makes a JSON-RPC call through the given eth-query object.
* *
* @param {any} ethQuery - The eth-query object. * @param ethQuery - The eth-query object.
* @param {object} request - The request data. * @param request - The request data.
* @returns {Promise<any>} A promise that either resolves with the result from * @returns A promise that either resolves with the result from the JSON-RPC
* the JSON-RPC response if it is successful or rejects with the error from the * response if it is successful or rejects with the error from the JSON-RPC
* JSON-RPC response otherwise. * response otherwise.
*/ */
function makeRpcCall(ethQuery, request) { function makeRpcCall(ethQuery: EthQuery, request: Request) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
debug('[makeRpcCall] making request', request); debug('[makeRpcCall] making request', request);
ethQuery.sendAsync(request, (error, result) => { ethQuery.sendAsync(request, (error, result) => {
@ -227,41 +276,43 @@ function makeRpcCall(ethQuery, request) {
}); });
} }
/** export type ProviderType = 'infura' | 'custom';
* @typedef {{providerType: 'infura' | 'custom', infuraNetwork?: string}} WithMockedCommunicationsOptions
*
* The options bag that `Communications` takes.
*/
/** export type MockOptions = {
* @typedef {{mockNextBlockTrackerRequest: (options: Omit<MockBlockTrackerRequestOptions, 'nockScope'>) => void, mockAllBlockTrackerRequests: (options: Omit<MockBlockTrackerRequestOptions, 'nockScope'>) => void, mockRpcCall: (options: Omit<MockRpcCallOptions, 'nockScope'>) => NockScope, rpcUrl: string, infuraNetwork: string}} Communications infuraNetwork?: BuiltInInfuraNetwork;
* providerType: ProviderType;
* Provides methods to mock different kinds of requests to the provider. customRpcUrl?: string;
*/ customChainId?: Hex;
};
/** export type MockCommunications = {
* @typedef {(comms: Communications) => Promise<any>} WithMockedCommunicationsCallback mockNextBlockTrackerRequest: (options?: any) => void;
* mockAllBlockTrackerRequests: (options?: any) => void;
* The callback that `mockingCommunications` takes. mockRpcCall: (options: CurriedMockRpcCallOptions) => MockRpcCallResult;
*/ rpcUrl: string;
infuraNetwork: BuiltInInfuraNetwork;
};
/** /**
* Sets up request mocks for requests to the provider. * Sets up request mocks for requests to the provider.
* *
* @param {WithMockedCommunicationsOptions} options - An options bag. * @param options - An options bag.
* @param {"infura" | "custom"} options.providerType - The type of network * @param options.providerType - The type of network client being tested.
* client being tested. * @param options.infuraNetwork - The name of the Infura network being tested,
* @param {string} [options.infuraNetwork] - The name of the Infura network being * assuming that `providerType` is "infura" (default: "mainnet").
* tested, assuming that `providerType` is "infura" (default: "mainnet"). * @param options.customRpcUrl - The URL of the custom RPC endpoint, assuming
* @param {string} [options.customRpcUrl] - The URL of the custom RPC endpoint, * that `providerType` is "custom".
* assuming that `providerType` is "custom". * @param fn - A function which will be called with an object that allows
* @param {WithMockedCommunicationsCallback} fn - A function which will be * interaction with the network client.
* called with an object that allows interaction with the network client. * @returns The return value of the given function.
* @returns {Promise<any>} The return value of the given function.
*/ */
export async function withMockedCommunications( 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') { if (providerType !== 'infura' && providerType !== 'custom') {
throw new Error( throw new Error(
@ -274,11 +325,11 @@ export async function withMockedCommunications(
? `https://${infuraNetwork}.infura.io` ? `https://${infuraNetwork}.infura.io`
: customRpcUrl; : customRpcUrl;
const nockScope = buildScopeForMockingRequests(rpcUrl); const nockScope = buildScopeForMockingRequests(rpcUrl);
const curriedMockNextBlockTrackerRequest = (localOptions) => const curriedMockNextBlockTrackerRequest = (localOptions: any) =>
mockNextBlockTrackerRequest({ nockScope, ...localOptions }); mockNextBlockTrackerRequest({ nockScope, ...localOptions });
const curriedMockAllBlockTrackerRequests = (localOptions) => const curriedMockAllBlockTrackerRequests = (localOptions: any) =>
mockAllBlockTrackerRequests({ nockScope, ...localOptions }); mockAllBlockTrackerRequests({ nockScope, ...localOptions });
const curriedMockRpcCall = (localOptions) => const curriedMockRpcCall = (localOptions: any) =>
mockRpcCall({ nockScope, ...localOptions }); mockRpcCall({ nockScope, ...localOptions });
const comms = { const comms = {
@ -297,12 +348,12 @@ export async function withMockedCommunications(
} }
} }
/** type MockNetworkClient = {
* @typedef {{blockTracker: import('eth-block-tracker').PollingBlockTracker, clock: sinon.SinonFakeTimers, makeRpcCall: (request: Partial<JsonRpcRequest>) => Promise<any>, makeRpcCallsInSeries: (requests: Partial<JsonRpcRequest>[]) => Promise<any>}} MockNetworkClient blockTracker: any;
* clock: sinon.SinonFakeTimers;
* Provides methods to interact with the suite of middleware that makeRpcCall: (request: Request) => Promise<any>;
* `createInfuraClient` or `createJsonRpcClient` exposes. makeRpcCallsInSeries: (requests: Request[]) => Promise<any[]>;
*/ };
/** /**
* Some middleware contain logic which retries the request if some condition * Some middleware contain logic which retries the request if some condition
@ -321,14 +372,14 @@ export async function withMockedCommunications(
* `setTimeout` handler. * `setTimeout` handler.
*/ */
export async function waitForPromiseToBeFulfilledAfterRunningAllTimers( export async function waitForPromiseToBeFulfilledAfterRunningAllTimers(
promise, promise: any,
clock, clock: any,
) { ) {
let hasPromiseBeenFulfilled = false; let hasPromiseBeenFulfilled = false;
let numTimesClockHasBeenAdvanced = 0; let numTimesClockHasBeenAdvanced = 0;
promise promise
.catch((error) => { .catch((error: any) => {
// This is used to silence Node.js warnings about the rejection // This is used to silence Node.js warnings about the rejection
// being handled asynchronously. The error is handled later when // being handled asynchronously. The error is handled later when
// `promise` is awaited, but we log it here anyway in case it gets // `promise` is awaited, but we log it here anyway in case it gets
@ -350,36 +401,22 @@ export async function waitForPromiseToBeFulfilledAfterRunningAllTimers(
return promise; 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 * 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 * block tracker, runs the given function with those two things, and then
* ensures the block tracker is stopped at the end. * ensures the block tracker is stopped at the end.
* *
* @param {WithClientOptions} options - An options bag. * @param options - An options bag.
* @param {"infura" | "custom"} options.providerType - The type of network * @param options.providerType - The type of network client being tested.
* client being tested. * @param options.infuraNetwork - The name of the Infura network being tested,
* @param {string} [options.infuraNetwork] - The name of the Infura network being * assuming that `providerType` is "infura" (default: "mainnet").
* tested, assuming that `providerType` is "infura" (default: "mainnet"). * @param options.customRpcUrl - The URL of the custom RPC endpoint, assuming
* @param {string} [options.customRpcUrl] - The URL of the custom RPC endpoint, * that `providerType` is "custom".
* assuming that `providerType` is "custom". * @param options.customChainId - The chain id belonging to the custom RPC
* @param {string} [options.customChainId] - The chain id belonging to the * endpoint, assuming that `providerType` is "custom" (default: "0x1").
* custom RPC endpoint, assuming that `providerType` is "custom" (default: * @param fn - A function which will be called with an object that allows
* "0x1"). * interaction with the network client.
* @param {WithClientCallback} fn - A function which will be called with an * @returns The return value of the given function.
* object that allows interaction with the network client.
* @returns {Promise<any>} The return value of the given function.
*/ */
export async function withNetworkClient( export async function withNetworkClient(
{ {
@ -387,8 +424,8 @@ export async function withNetworkClient(
infuraNetwork = 'mainnet', infuraNetwork = 'mainnet',
customRpcUrl = MOCK_RPC_URL, customRpcUrl = MOCK_RPC_URL,
customChainId = '0x1', customChainId = '0x1',
}, }: MockOptions,
fn, fn: (client: MockNetworkClient) => Promise<any>,
) { ) {
if (providerType !== 'infura' && providerType !== 'custom') { if (providerType !== 'infura' && providerType !== 'custom') {
throw new Error( throw new Error(
@ -414,20 +451,21 @@ export async function withNetworkClient(
? createNetworkClient({ ? createNetworkClient({
network: infuraNetwork, network: infuraNetwork,
infuraProjectId: MOCK_INFURA_PROJECT_ID, infuraProjectId: MOCK_INFURA_PROJECT_ID,
type: 'infura', type: NetworkClientType.Infura,
}) })
: createNetworkClient({ : createNetworkClient({
chainId: customChainId, chainId: customChainId,
rpcUrl: customRpcUrl, rpcUrl: customRpcUrl,
type: 'custom', type: NetworkClientType.Custom,
}); });
process.env.IN_TEST = inTest; process.env.IN_TEST = inTest;
const { provider, blockTracker } = clientUnderTest; const { provider, blockTracker } = clientUnderTest;
const ethQuery = new EthQuery(provider); const ethQuery = new EthQuery(provider);
const curriedMakeRpcCall = (request) => makeRpcCall(ethQuery, request); const curriedMakeRpcCall = (request: Request) =>
const makeRpcCallsInSeries = async (requests) => { makeRpcCall(ethQuery, request);
const makeRpcCallsInSeries = async (requests: Request[]) => {
const responses = []; const responses = [];
for (const request of requests) { for (const request of requests) {
responses.push(await curriedMakeRpcCall(request)); 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. * 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 * The block parameter can be set to a custom value. If no value is given, it
* is set as undefined. * is set as undefined.
* *
* @param {object} args - Arguments. * @param args - Arguments.
* @param {number} args.blockParamIndex - The index of the block parameter. * @param args.blockParamIndex - The index of the block parameter.
* @param {any} [args.blockParam] - The block parameter value to set. * @param args.blockParam - The block parameter value to set.
* @returns {any[]} The mock params. * @returns The mock params.
*/ */
export function buildMockParams({ blockParam, blockParamIndex }) { export function buildMockParams({
if (blockParamIndex === undefined) { blockParam,
throw new Error(`Missing 'blockParamIndex'`); blockParamIndex,
} }: BuildMockParamsOptions) {
const params = new Array(blockParamIndex).fill('some value'); const params = new Array(blockParamIndex).fill('some value');
params[blockParamIndex] = blockParam; params[blockParamIndex] = blockParam;
@ -480,18 +524,18 @@ export function buildMockParams({ blockParam, blockParamIndex }) {
* Returns a partial JSON-RPC request object, with the "block" param replaced * Returns a partial JSON-RPC request object, with the "block" param replaced
* with the given value. * with the given value.
* *
* @param {object} request - The request object. * @param request - The request object.
* @param {string} request.method - The request method. * @param request.method - The request method.
* @param {params} [request.params] - The request params. * @param request.params - The request params.
* @param {number} blockParamIndex - The index within the `params` array of the * @param blockParamIndex - The index within the `params` array of the block
* block param. * param.
* @param {any} blockParam - The desired block param value. * @param blockParam - The desired block param value.
* @returns {object} The updated request object. * @returns The updated request object.
*/ */
export function buildRequestWithReplacedBlockParam( export function buildRequestWithReplacedBlockParam(
{ method, params = [] }, { method, params = [] }: Request,
blockParamIndex, blockParamIndex: number,
blockParam, blockParam: any,
) { ) {
const updatedParams = params.slice(); const updatedParams = params.slice();
updatedParams[blockParamIndex] = blockParam; updatedParams[blockParamIndex] = blockParam;

View File

@ -1,6 +1,7 @@
/* eslint-disable jest/require-top-level-describe, jest/no-export */ /* eslint-disable jest/require-top-level-describe, jest/no-export */
import { import {
ProviderType,
waitForPromiseToBeFulfilledAfterRunningAllTimers, waitForPromiseToBeFulfilledAfterRunningAllTimers,
withMockedCommunications, withMockedCommunications,
withNetworkClient, withNetworkClient,
@ -11,6 +12,11 @@ import {
buildJsonRpcEngineEmptyResponseErrorMessage, buildJsonRpcEngineEmptyResponseErrorMessage,
} from './shared-tests'; } from './shared-tests';
type TestsForRpcMethodAssumingNoBlockParamOptions = {
providerType: ProviderType;
numberOfParameters: number;
};
/** /**
* Defines tests which exercise the behavior exhibited by an RPC method which is * 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 * 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"). * either `infura` or `custom` (default: "infura").
*/ */
export function testsForRpcMethodAssumingNoBlockParam( export function testsForRpcMethodAssumingNoBlockParam(
method, method: string,
{ numberOfParameters, providerType }, {
numberOfParameters,
providerType,
}: TestsForRpcMethodAssumingNoBlockParamOptions,
) { ) {
if (providerType !== 'infura' && providerType !== 'custom') { if (providerType !== 'infura' && providerType !== 'custom') {
throw new Error( throw new Error(

View File

@ -1,7 +1,16 @@
/* eslint-disable jest/require-top-level-describe, jest/no-export */ /* eslint-disable jest/require-top-level-describe, jest/no-export */
import { fill } from 'lodash'; 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 * Defines tests which exercise the behavior exhibited by an RPC method that
@ -15,8 +24,11 @@ import { withMockedCommunications, withNetworkClient } from './helpers';
* RPC method takes. * RPC method takes.
*/ */
export function testsForRpcMethodNotHandledByMiddleware( export function testsForRpcMethodNotHandledByMiddleware(
method, method: string,
{ providerType, numberOfParameters }, {
providerType,
numberOfParameters,
}: TestsForRpcMethodNotHandledByMiddlewareOptions,
) { ) {
if (providerType !== 'infura' && providerType !== 'custom') { if (providerType !== 'infura' && providerType !== 'custom') {
throw new Error( throw new Error(

View File

@ -2,7 +2,11 @@
import { testsForRpcMethodsThatCheckForBlockHashInResponse } from './block-hash-in-response'; import { testsForRpcMethodsThatCheckForBlockHashInResponse } from './block-hash-in-response';
import { testsForRpcMethodSupportingBlockParam } from './block-param'; import { testsForRpcMethodSupportingBlockParam } from './block-param';
import { withMockedCommunications, withNetworkClient } from './helpers'; import {
ProviderType,
withMockedCommunications,
withNetworkClient,
} from './helpers';
import { testsForRpcMethodAssumingNoBlockParam } from './no-block-param'; import { testsForRpcMethodAssumingNoBlockParam } from './no-block-param';
import { testsForRpcMethodNotHandledByMiddleware } from './not-handled-by-middleware'; 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. * @param reason - The exact reason for failure.
* @returns The error message. * @returns The error message.
*/ */
export function buildInfuraClientRetriesExhaustedErrorMessage(reason) { export function buildInfuraClientRetriesExhaustedErrorMessage(reason: string) {
return new RegExp( return new RegExp(
`^InfuraProvider - cannot complete request. All retries exhausted\\..+${reason}`, `^InfuraProvider - cannot complete request. All retries exhausted\\..+${reason}`,
'us', 'us',
@ -27,7 +31,7 @@ export function buildInfuraClientRetriesExhaustedErrorMessage(reason) {
* @param method - The RPC method. * @param method - The RPC method.
* @returns The error message. * @returns The error message.
*/ */
export function buildJsonRpcEngineEmptyResponseErrorMessage(method) { export function buildJsonRpcEngineEmptyResponseErrorMessage(method: string) {
return new RegExp( return new RegExp(
`^JsonRpcEngine: Response has no error or result for request:.+"method": "${method}"`, `^JsonRpcEngine: Response has no error or result for request:.+"method": "${method}"`,
'us', 'us',
@ -42,7 +46,7 @@ export function buildJsonRpcEngineEmptyResponseErrorMessage(method) {
* @param reason - The reason. * @param reason - The reason.
* @returns The error message. * @returns The error message.
*/ */
export function buildFetchFailedErrorMessage(url, reason) { export function buildFetchFailedErrorMessage(url: string, reason: string) {
return new RegExp( return new RegExp(
`^request to ${url}(/[^/ ]*)+ failed, reason: ${reason}`, `^request to ${url}(/[^/ ]*)+ failed, reason: ${reason}`,
'us', 'us',
@ -57,7 +61,7 @@ export function buildFetchFailedErrorMessage(url, reason) {
* exposed by `createInfuraClient` is tested; if `custom`, then the middleware * exposed by `createInfuraClient` is tested; if `custom`, then the middleware
* exposed by `createJsonRpcClient` will be tested. * 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/> // Ethereum JSON-RPC spec: <https://ethereum.github.io/execution-apis/api-documentation/>
// Infura documentation: <https://docs.infura.io/infura/networks/ethereum/json-rpc-methods> // 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 { TokenListController } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '../../../shared/constants/network'; import { CHAIN_IDS } from '../../../shared/constants/network';
import PreferencesController from './preferences'; import PreferencesController from './preferences';
import NetworkController from './network'; import { NetworkController } from './network';
describe('preferences controller', function () { describe('preferences controller', function () {
let preferencesController; 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 () => { it('rejects message on error', async () => {
keyringControllerMock[signMethodName].mockReset(); keyringControllerMock[signMethodName].mockReset();
keyringControllerMock[signMethodName].mockRejectedValue( keyringControllerMock[signMethodName].mockRejectedValue(
@ -468,6 +476,14 @@ describe('SignController', () => {
'Cancel', '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', () => { describe('message manager events', () => {

View File

@ -33,12 +33,13 @@ import {
RejectRequest, RejectRequest,
} from '@metamask/approval-controller'; } from '@metamask/approval-controller';
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import PreferencesController from './preferences'; import PreferencesController from './preferences';
const controllerName = 'SignController'; const controllerName = 'SignController';
const methodNameSign = 'eth_sign'; const methodNameSign = MESSAGE_TYPE.ETH_SIGN;
const methodNamePersonalSign = 'personal_sign'; const methodNamePersonalSign = MESSAGE_TYPE.PERSONAL_SIGN;
const methodNameTypedSign = 'eth_signTypedData'; const methodNameTypedSign = MESSAGE_TYPE.ETH_SIGN_TYPED_DATA;
const stateMetadata = { const stateMetadata = {
unapprovedMsgs: { persist: false, anonymous: false }, unapprovedMsgs: { persist: false, anonymous: false },
@ -104,7 +105,6 @@ export type SignControllerOptions = {
messenger: SignControllerMessenger; messenger: SignControllerMessenger;
keyringController: KeyringController; keyringController: KeyringController;
preferencesController: PreferencesController; preferencesController: PreferencesController;
sendUpdate: () => void;
getState: () => any; getState: () => any;
metricsEvent: (payload: any, options?: any) => void; metricsEvent: (payload: any, options?: any) => void;
securityProviderRequest: ( securityProviderRequest: (
@ -637,14 +637,22 @@ export default class SignController extends BaseControllerV2<
} }
private _acceptApproval(messageId: string) { 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) { private _rejectApproval(messageId: string) {
this.messagingSystem.call( try {
'ApprovalController:rejectRequest', this.messagingSystem.call(
messageId, 'ApprovalController:rejectRequest',
'Cancel', messageId,
); 'Cancel',
);
} catch (error) {
log.info('Failed to reject signature approval request', error);
}
} }
} }

View File

@ -52,7 +52,10 @@ import {
determineTransactionType, determineTransactionType,
isEIP1559Transaction, isEIP1559Transaction,
} from '../../../../shared/modules/transaction.utils'; } from '../../../../shared/modules/transaction.utils';
import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; import {
ORIGIN_METAMASK,
MESSAGE_TYPE,
} from '../../../../shared/constants/app';
import { import {
calcGasTotal, calcGasTotal,
getSwapsTokensReceivedFromTxMeta, getSwapsTokensReceivedFromTxMeta,
@ -156,6 +159,7 @@ export default class TransactionController extends EventEmitter {
this.getAccountType = opts.getAccountType; this.getAccountType = opts.getAccountType;
this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails; this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails;
this.securityProviderRequest = opts.securityProviderRequest; this.securityProviderRequest = opts.securityProviderRequest;
this.messagingSystem = opts.messenger;
this.memStore = new ObservableStore({}); this.memStore = new ObservableStore({});
@ -798,6 +802,7 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.getTransactionWithActionId(actionId); this.txStateManager.getTransactionWithActionId(actionId);
if (existingTxMeta) { if (existingTxMeta) {
this.emit('newUnapprovedTx', existingTxMeta); this.emit('newUnapprovedTx', existingTxMeta);
this._requestApproval(existingTxMeta);
existingTxMeta = await this.addTransactionGasDefaults(existingTxMeta); existingTxMeta = await this.addTransactionGasDefaults(existingTxMeta);
return existingTxMeta; return existingTxMeta;
} }
@ -870,6 +875,7 @@ export default class TransactionController extends EventEmitter {
this.addTransaction(txMeta); this.addTransaction(txMeta);
this.emit('newUnapprovedTx', txMeta); this.emit('newUnapprovedTx', txMeta);
this._requestApproval(txMeta);
txMeta = await this.addTransactionGasDefaults(txMeta); txMeta = await this.addTransactionGasDefaults(txMeta);
@ -1355,6 +1361,7 @@ export default class TransactionController extends EventEmitter {
try { try {
// approve // approve
this.txStateManager.setTxStatusApproved(txId); this.txStateManager.setTxStatusApproved(txId);
this._acceptApproval(txMeta);
// get next nonce // get next nonce
const fromAddress = txMeta.txParams.from; const fromAddress = txMeta.txParams.from;
// wait for a nonce // wait for a nonce
@ -1734,6 +1741,7 @@ export default class TransactionController extends EventEmitter {
async cancelTransaction(txId, actionId) { async cancelTransaction(txId, actionId) {
const txMeta = this.txStateManager.getTransaction(txId); const txMeta = this.txStateManager.getTransaction(txId);
this.txStateManager.setTxStatusRejected(txId); this.txStateManager.setTxStatusRejected(txId);
this._rejectApproval(txMeta);
this._trackTransactionMetricsEvent( this._trackTransactionMetricsEvent(
txMeta, txMeta,
TransactionMetaMetricsEvent.rejected, 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, GasRecommendations,
} from '../../../../shared/constants/gas'; } from '../../../../shared/constants/gas';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; 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 { NetworkStatus } from '../../../../shared/constants/network';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils'; import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
import TransactionController from '.'; import TransactionController from '.';
@ -52,7 +55,8 @@ describe('Transaction Controller', function () {
fromAccount, fromAccount,
fragmentExists, fragmentExists,
networkStatusStore, networkStatusStore,
getCurrentChainId; getCurrentChainId,
messengerMock;
beforeEach(function () { beforeEach(function () {
fragmentExists = false; fragmentExists = false;
@ -76,6 +80,7 @@ describe('Transaction Controller', function () {
blockTrackerStub.getLatestBlock = noop; blockTrackerStub.getLatestBlock = noop;
getCurrentChainId = sinon.stub().callsFake(() => currentChainId); getCurrentChainId = sinon.stub().callsFake(() => currentChainId);
messengerMock = { call: sinon.stub().returns(Promise.resolve()) };
txController = new TransactionController({ txController = new TransactionController({
provider, provider,
@ -108,6 +113,7 @@ describe('Transaction Controller', function () {
getAccountType: () => 'MetaMask', getAccountType: () => 'MetaMask',
getDeviceModel: () => 'N/A', getDeviceModel: () => 'N/A',
securityProviderRequest: () => undefined, securityProviderRequest: () => undefined,
messenger: messengerMock,
}); });
txController.nonceTracker.getNonceLock = () => txController.nonceTracker.getNonceLock = () =>
Promise.resolve({ nextNonce: 0, releaseLock: noop }); Promise.resolve({ nextNonce: 0, releaseLock: noop });
@ -489,6 +495,67 @@ describe('Transaction Controller', function () {
{ message: 'MetaMask is having trouble connecting to the network' }, { 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 () { describe('#createCancelTransaction', function () {
@ -997,9 +1064,11 @@ describe('Transaction Controller', function () {
}); });
describe('#approveTransaction', function () { describe('#approveTransaction', function () {
it('does not overwrite set values', async function () { let originalValue, txMeta, signStub, pubStub;
const originalValue = '0x01';
const txMeta = { beforeEach(function () {
originalValue = '0x01';
txMeta = {
id: '1', id: '1',
status: TransactionStatus.unapproved, status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
@ -1019,17 +1088,22 @@ describe('Transaction Controller', function () {
providerResultStub.eth_gasPrice = wrongValue; providerResultStub.eth_gasPrice = wrongValue;
providerResultStub.eth_estimateGas = '0x5209'; providerResultStub.eth_estimateGas = '0x5209';
const signStub = sinon signStub = sinon
.stub(txController, 'signTransaction') .stub(txController, 'signTransaction')
.callsFake(() => Promise.resolve()); .callsFake(() => Promise.resolve());
const pubStub = sinon pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => {
.stub(txController, 'publishTransaction') txController.setTxHash('1', originalValue);
.callsFake(() => { txController.txStateManager.setTxStatusSubmitted('1');
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); await txController.approveTransaction(txMeta.id);
const result = txController.txStateManager.getTransaction(txMeta.id); const result = txController.txStateManager.getTransaction(txMeta.id);
const params = result.txParams; const params = result.txParams;
@ -1042,8 +1116,21 @@ describe('Transaction Controller', function () {
TransactionStatus.submitted, TransactionStatus.submitted,
'should have reached the submitted status.', '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 () { describe('#cancelTransaction', function () {
it('should emit a status change to rejected', function (done) { beforeEach(function () {
txController.txStateManager._addTransactionsToState([ txController.txStateManager._addTransactionsToState([
{ {
id: 0, id: 0,
@ -1181,7 +1268,9 @@ describe('Transaction Controller', function () {
history: [{}], history: [{}],
}, },
]); ]);
});
it('should emit a status change to rejected', function (done) {
txController.once('tx:status-update', (txId, status) => { txController.once('tx:status-update', (txId, status) => {
try { try {
assert.equal( assert.equal(
@ -1198,6 +1287,22 @@ describe('Transaction Controller', function () {
txController.cancelTransaction(0); 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 () { 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 { MILLISECOND, SECOND } from '../../shared/constants/time';
import { import {
ORIGIN_METAMASK, ORIGIN_METAMASK,
///: BEGIN:ONLY_INCLUDE_IN(flask)
MESSAGE_TYPE, MESSAGE_TYPE,
///: BEGIN:ONLY_INCLUDE_IN(flask)
SNAP_DIALOG_TYPES, SNAP_DIALOG_TYPES,
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
POLLING_TOKEN_ENVIRONMENT_TYPES, POLLING_TOKEN_ENVIRONMENT_TYPES,
@ -143,8 +143,9 @@ import createTabIdMiddleware from './lib/createTabIdMiddleware';
import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware';
import { setupMultiplex } from './lib/stream-utils'; import { setupMultiplex } from './lib/stream-utils';
import EnsController from './controllers/ens'; import EnsController from './controllers/ens';
import NetworkController, { import {
NetworkControllerEventTypes, NetworkController,
NetworkControllerEventType,
} from './controllers/network'; } from './controllers/network';
import PreferencesController from './controllers/preferences'; import PreferencesController from './controllers/preferences';
import AppStateController from './controllers/app-state'; import AppStateController from './controllers/app-state';
@ -154,7 +155,6 @@ import OnboardingController from './controllers/onboarding';
import BackupController from './controllers/backup'; import BackupController from './controllers/backup';
import IncomingTransactionsController from './controllers/incoming-transactions'; import IncomingTransactionsController from './controllers/incoming-transactions';
import DecryptMessageManager from './lib/decrypt-message-manager'; import DecryptMessageManager from './lib/decrypt-message-manager';
import EncryptionPublicKeyManager from './lib/encryption-public-key-manager';
import TransactionController from './controllers/transactions'; import TransactionController from './controllers/transactions';
import DetectTokensController from './controllers/detect-tokens'; import DetectTokensController from './controllers/detect-tokens';
import SwapsController from './controllers/swaps'; import SwapsController from './controllers/swaps';
@ -166,6 +166,7 @@ import createMetaRPCHandler from './lib/createMetaRPCHandler';
import { previousValueComparator } from './lib/util'; import { previousValueComparator } from './lib/util';
import createMetamaskMiddleware from './lib/createMetamaskMiddleware'; import createMetamaskMiddleware from './lib/createMetamaskMiddleware';
import SignController from './controllers/sign'; import SignController from './controllers/sign';
import EncryptionPublicKeyController from './controllers/encryption-public-key';
import { import {
CaveatMutatorFactories, CaveatMutatorFactories,
@ -259,11 +260,16 @@ export default class MetamaskController extends EventEmitter {
name: 'ApprovalController', name: 'ApprovalController',
}), }),
showApprovalRequest: opts.showUserConfirmation, showApprovalRequest: opts.showUserConfirmation,
typesExcludedFromRateLimiting: [
MESSAGE_TYPE.ETH_SIGN,
MESSAGE_TYPE.PERSONAL_SIGN,
MESSAGE_TYPE.ETH_SIGN_TYPED_DATA,
],
}); });
const networkControllerMessenger = this.controllerMessenger.getRestricted({ const networkControllerMessenger = this.controllerMessenger.getRestricted({
name: 'NetworkController', name: 'NetworkController',
allowedEvents: Object.values(NetworkControllerEventTypes), allowedEvents: Object.values(NetworkControllerEventType),
}); });
this.networkController = new NetworkController({ this.networkController = new NetworkController({
messenger: networkControllerMessenger, messenger: networkControllerMessenger,
@ -310,11 +316,11 @@ export default class MetamaskController extends EventEmitter {
initLangCode: opts.initLangCode, initLangCode: opts.initLangCode,
onInfuraIsBlocked: networkControllerMessenger.subscribe.bind( onInfuraIsBlocked: networkControllerMessenger.subscribe.bind(
networkControllerMessenger, networkControllerMessenger,
NetworkControllerEventTypes.InfuraIsBlocked, NetworkControllerEventType.InfuraIsBlocked,
), ),
onInfuraIsUnblocked: networkControllerMessenger.subscribe.bind( onInfuraIsUnblocked: networkControllerMessenger.subscribe.bind(
networkControllerMessenger, networkControllerMessenger,
NetworkControllerEventTypes.InfuraIsUnblocked, NetworkControllerEventType.InfuraIsUnblocked,
), ),
tokenListController: this.tokenListController, tokenListController: this.tokenListController,
provider: this.provider, provider: this.provider,
@ -452,7 +458,7 @@ export default class MetamaskController extends EventEmitter {
preferencesStore: this.preferencesController.store, preferencesStore: this.preferencesController.store,
onNetworkDidChange: networkControllerMessenger.subscribe.bind( onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger, networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
), ),
getNetworkIdentifier: () => { getNetworkIdentifier: () => {
const { type, rpcUrl } = const { type, rpcUrl } =
@ -491,7 +497,7 @@ export default class MetamaskController extends EventEmitter {
// onNetworkDidChange // onNetworkDidChange
onNetworkStateChange: networkControllerMessenger.subscribe.bind( onNetworkStateChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger, networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
), ),
getCurrentNetworkEIP1559Compatibility: getCurrentNetworkEIP1559Compatibility:
this.networkController.getEIP1559Compatibility.bind( this.networkController.getEIP1559Compatibility.bind(
@ -519,9 +525,15 @@ export default class MetamaskController extends EventEmitter {
isUnlocked: this.isUnlocked.bind(this), isUnlocked: this.isUnlocked.bind(this),
initState: initState.AppStateController, initState: initState.AppStateController,
onInactiveTimeout: () => this.setLocked(), onInactiveTimeout: () => this.setLocked(),
showUnlockRequest: opts.showUserConfirmation,
preferencesStore: this.preferencesController.store, preferencesStore: this.preferencesController.store,
qrHardwareStore: this.qrHardwareKeyring.getMemStore(), qrHardwareStore: this.qrHardwareKeyring.getMemStore(),
messenger: this.controllerMessenger.getRestricted({
name: 'AppStateController',
allowedActions: [
`${this.approvalController.name}:addRequest`,
`${this.approvalController.name}:acceptRequest`,
],
}),
}); });
const currencyRateMessenger = this.controllerMessenger.getRestricted({ const currencyRateMessenger = this.controllerMessenger.getRestricted({
@ -609,7 +621,7 @@ export default class MetamaskController extends EventEmitter {
this.networkController.store.getState().provider.chainId, this.networkController.store.getState().provider.chainId,
onNetworkDidChange: networkControllerMessenger.subscribe.bind( onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger, networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
), ),
}); });
@ -621,7 +633,7 @@ export default class MetamaskController extends EventEmitter {
blockTracker: this.blockTracker, blockTracker: this.blockTracker,
onNetworkDidChange: networkControllerMessenger.subscribe.bind( onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger, networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
), ),
getCurrentChainId: () => getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId, this.networkController.store.getState().provider.chainId,
@ -1007,8 +1019,15 @@ export default class MetamaskController extends EventEmitter {
getDeviceModel: this.getDeviceModel.bind(this), getDeviceModel: this.getDeviceModel.bind(this),
getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this), getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this),
securityProviderRequest: this.securityProviderRequest.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) => { this.txController.on(`tx:status-update`, async (txId, status) => {
if ( if (
@ -1099,7 +1118,7 @@ export default class MetamaskController extends EventEmitter {
}); });
networkControllerMessenger.subscribe( networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
async () => { async () => {
const { ticker } = this.networkController.store.getState().provider; const { ticker } = this.networkController.store.getState().provider;
try { try {
@ -1117,7 +1136,18 @@ export default class MetamaskController extends EventEmitter {
this.metaMetricsController, 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( metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController, this.metaMetricsController,
), ),
@ -1145,7 +1175,7 @@ export default class MetamaskController extends EventEmitter {
networkController: this.networkController, networkController: this.networkController,
onNetworkDidChange: networkControllerMessenger.subscribe.bind( onNetworkDidChange: networkControllerMessenger.subscribe.bind(
networkControllerMessenger, networkControllerMessenger,
NetworkControllerEventTypes.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
), ),
provider: this.provider, provider: this.provider,
getProviderConfig: () => this.networkController.store.getState().provider, getProviderConfig: () => this.networkController.store.getState().provider,
@ -1188,7 +1218,7 @@ export default class MetamaskController extends EventEmitter {
// ensure accountTracker updates balances after network change // ensure accountTracker updates balances after network change
networkControllerMessenger.subscribe( networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkDidChange, NetworkControllerEventType.NetworkDidChange,
() => { () => {
this.accountTracker._updateAccounts(); this.accountTracker._updateAccounts();
}, },
@ -1196,10 +1226,10 @@ export default class MetamaskController extends EventEmitter {
// clear unapproved transactions and messages when the network will change // clear unapproved transactions and messages when the network will change
networkControllerMessenger.subscribe( networkControllerMessenger.subscribe(
NetworkControllerEventTypes.NetworkWillChange, NetworkControllerEventType.NetworkWillChange,
() => { () => {
this.txController.txStateManager.clearUnapprovedTxs(); this.txController.txStateManager.clearUnapprovedTxs();
this.encryptionPublicKeyManager.clearUnapproved(); this.encryptionPublicKeyController.clearUnapproved();
this.decryptMessageManager.clearUnapproved(); this.decryptMessageManager.clearUnapproved();
this.signController.clearUnapproved(); this.signController.clearUnapproved();
}, },
@ -1266,7 +1296,10 @@ export default class MetamaskController extends EventEmitter {
this.signController, this.signController,
), ),
processDecryptMessage: this.newRequestDecryptMessage.bind(this), processDecryptMessage: this.newRequestDecryptMessage.bind(this),
processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this), processEncryptionPublicKey:
this.encryptionPublicKeyController.newRequestEncryptionPublicKey.bind(
this.encryptionPublicKeyController,
),
getPendingNonce: this.getPendingNonce.bind(this), getPendingNonce: this.getPendingNonce.bind(this),
getPendingTransactionByHash: (hash) => getPendingTransactionByHash: (hash) =>
this.txController.getTransactions({ this.txController.getTransactions({
@ -1289,7 +1322,7 @@ export default class MetamaskController extends EventEmitter {
TxController: this.txController.memStore, TxController: this.txController.memStore,
TokenRatesController: this.tokenRatesController, TokenRatesController: this.tokenRatesController,
DecryptMessageManager: this.decryptMessageManager.memStore, DecryptMessageManager: this.decryptMessageManager.memStore,
EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore, EncryptionPublicKeyController: this.encryptionPublicKeyController,
SignController: this.signController, SignController: this.signController,
SwapsController: this.swapsController.store, SwapsController: this.swapsController.store,
EnsController: this.ensController.store, EnsController: this.ensController.store,
@ -1371,7 +1404,9 @@ export default class MetamaskController extends EventEmitter {
this.accountTracker.resetState, this.accountTracker.resetState,
this.txController.resetState, this.txController.resetState,
this.decryptMessageManager.resetState, this.decryptMessageManager.resetState,
this.encryptionPublicKeyManager.resetState, this.encryptionPublicKeyController.resetState.bind(
this.encryptionPublicKeyController,
),
this.signController.resetState.bind(this.signController), this.signController.resetState.bind(this.signController),
this.swapsController.resetState, this.swapsController.resetState,
this.ensController.resetState, this.ensController.resetState,
@ -1994,6 +2029,16 @@ export default class MetamaskController extends EventEmitter {
appStateController.setConnectedStatusPopoverHasBeenShown.bind( appStateController.setConnectedStatusPopoverHasBeenShown.bind(
appStateController, appStateController,
), ),
setRecoveryPhraseReminderHasBeenShown:
appStateController.setRecoveryPhraseReminderHasBeenShown.bind(
appStateController,
),
setRecoveryPhraseReminderLastShown:
appStateController.setRecoveryPhraseReminderLastShown.bind(
appStateController,
),
setTermsOfUseLastAgreed:
appStateController.setTermsOfUseLastAgreed.bind(appStateController),
setOutdatedBrowserWarningLastShown: setOutdatedBrowserWarningLastShown:
appStateController.setOutdatedBrowserWarningLastShown.bind( appStateController.setOutdatedBrowserWarningLastShown.bind(
appStateController, appStateController,
@ -2074,9 +2119,15 @@ export default class MetamaskController extends EventEmitter {
decryptMessageInline: this.decryptMessageInline.bind(this), decryptMessageInline: this.decryptMessageInline.bind(this),
cancelDecryptMessage: this.cancelDecryptMessage.bind(this), cancelDecryptMessage: this.cancelDecryptMessage.bind(this),
// EncryptionPublicKeyManager // EncryptionPublicKeyController
encryptionPublicKey: this.encryptionPublicKey.bind(this), encryptionPublicKey:
cancelEncryptionPublicKey: this.cancelEncryptionPublicKey.bind(this), this.encryptionPublicKeyController.encryptionPublicKey.bind(
this.encryptionPublicKeyController,
),
cancelEncryptionPublicKey:
this.encryptionPublicKeyController.cancelEncryptionPublicKey.bind(
this.encryptionPublicKeyController,
),
// onboarding controller // onboarding controller
setSeedPhraseBackedUp: setSeedPhraseBackedUp:
@ -3301,109 +3352,6 @@ export default class MetamaskController extends EventEmitter {
return this.getState(); 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 * @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 m081 from './081';
import * as m082 from './082'; import * as m082 from './082';
import * as m083 from './083'; import * as m083 from './083';
import * as m084 from './084';
import * as m085 from './085';
const migrations = [ const migrations = [
m002, m002,
@ -171,6 +173,8 @@ const migrations = [
m081, m081,
m082, m082,
m083, m083,
m084,
m085,
]; ];
export default migrations; export default migrations;

View File

@ -6,10 +6,10 @@
// subset of files to check against these targets. // subset of files to check against these targets.
module.exports = { module.exports = {
global: { global: {
lines: 66, lines: 67.8,
branches: 54.4, branches: 55.84,
statements: 65, statements: 67.13,
functions: 58.5, functions: 59.66,
}, },
transforms: { transforms: {
branches: 100, branches: 100,

View File

@ -2,6 +2,7 @@ module.exports = {
collectCoverageFrom: [ collectCoverageFrom: [
'<rootDir>/app/scripts/constants/error-utils.js', '<rootDir>/app/scripts/constants/error-utils.js',
'<rootDir>/app/scripts/controllers/network/**/*.js', '<rootDir>/app/scripts/controllers/network/**/*.js',
'<rootDir>/app/scripts/controllers/network/**/*.ts',
'<rootDir>/app/scripts/controllers/permissions/**/*.js', '<rootDir>/app/scripts/controllers/permissions/**/*.js',
'<rootDir>/app/scripts/controllers/sign.ts', '<rootDir>/app/scripts/controllers/sign.ts',
'<rootDir>/app/scripts/flask/**/*.js', '<rootDir>/app/scripts/flask/**/*.js',
@ -39,6 +40,7 @@ module.exports = {
'<rootDir>/app/scripts/constants/error-utils.test.js', '<rootDir>/app/scripts/constants/error-utils.test.js',
'<rootDir>/app/scripts/controllers/app-state.test.js', '<rootDir>/app/scripts/controllers/app-state.test.js',
'<rootDir>/app/scripts/controllers/network/**/*.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/permissions/**/*.test.js',
'<rootDir>/app/scripts/controllers/sign.test.ts', '<rootDir>/app/scripts/controllers/sign.test.ts',
'<rootDir>/app/scripts/flask/**/*.test.js', '<rootDir>/app/scripts/flask/**/*.test.js',
@ -51,7 +53,7 @@ module.exports = {
'<rootDir>/ui/**/*.test.(js|ts|tsx)', '<rootDir>/ui/**/*.test.(js|ts|tsx)',
'<rootDir>/development/fitness-functions/**/*.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 // 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 // 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 // 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": { "@metamask/address-book-controller": {
"packages": { "packages": {
"@metamask/address-book-controller>@metamask/base-controller": true, "@metamask/base-controller": true,
"@metamask/address-book-controller>@metamask/controller-utils": true "@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/announcement-controller": { "@metamask/announcement-controller": {
@ -716,12 +696,12 @@
"@ethersproject/contracts": true, "@ethersproject/contracts": true,
"@ethersproject/providers": true, "@ethersproject/providers": true,
"@metamask/assets-controllers>@metamask/abi-utils": true, "@metamask/assets-controllers>@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/controller-utils": true,
"@metamask/assets-controllers>@metamask/utils": true, "@metamask/assets-controllers>@metamask/utils": true,
"@metamask/assets-controllers>abort-controller": true, "@metamask/assets-controllers>abort-controller": true,
"@metamask/assets-controllers>multiformats": true, "@metamask/assets-controllers>multiformats": true,
"@metamask/base-controller": true, "@metamask/base-controller": true,
"@metamask/contract-metadata": true, "@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/metamask-eth-abis": true, "@metamask/metamask-eth-abis": true,
"browserify>events": true, "browserify>events": true,
"eth-json-rpc-filters>async-mutex": true, "eth-json-rpc-filters>async-mutex": true,
@ -750,21 +730,6 @@
"semver": true "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": { "@metamask/assets-controllers>@metamask/utils": {
"globals": { "globals": {
"TextDecoder": true, "TextDecoder": true,
@ -1089,7 +1054,7 @@
"@metamask/eth-token-tracker>deep-equal>is-date-object": true, "@metamask/eth-token-tracker>deep-equal>is-date-object": true,
"@ngraveio/bc-ur>assert>object-is": true, "@ngraveio/bc-ur>assert>object-is": true,
"@storybook/api>telejson>is-regex": 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 "string.prototype.matchall>regexp.prototype.flags": true
} }
}, },
@ -1193,34 +1158,14 @@
"setInterval": true "setInterval": true
}, },
"packages": { "packages": {
"@metamask/gas-fee-controller>@metamask/base-controller": true, "@metamask/base-controller": true,
"@metamask/gas-fee-controller>@metamask/controller-utils": true, "@metamask/controller-utils": true,
"eth-query": true, "eth-query": true,
"ethereumjs-util": true, "ethereumjs-util": true,
"ethjs>ethjs-unit": true, "ethjs>ethjs-unit": true,
"uuid": 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": { "@metamask/jazzicon": {
"globals": { "globals": {
"document.createElement": true, "document.createElement": true,
@ -1319,8 +1264,8 @@
}, },
"@metamask/message-manager": { "@metamask/message-manager": {
"packages": { "packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true, "@metamask/controller-utils": true,
"@metamask/message-manager>@metamask/base-controller": true,
"@metamask/message-manager>jsonschema": true, "@metamask/message-manager>jsonschema": true,
"browserify>buffer": true, "browserify>buffer": true,
"browserify>events": true, "browserify>events": true,
@ -1329,11 +1274,6 @@
"uuid": true "uuid": true
} }
}, },
"@metamask/message-manager>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/message-manager>jsonschema": { "@metamask/message-manager>jsonschema": {
"packages": { "packages": {
"browserify>url": true "browserify>url": true
@ -1364,8 +1304,8 @@
"console.error": true "console.error": true
}, },
"packages": { "packages": {
"@metamask/base-controller": true,
"@metamask/controller-utils": true, "@metamask/controller-utils": true,
"@metamask/permission-controller>@metamask/base-controller": true,
"@metamask/permission-controller>nanoid": true, "@metamask/permission-controller>nanoid": true,
"deep-freeze-strict": true, "deep-freeze-strict": true,
"eth-rpc-errors": true, "eth-rpc-errors": true,
@ -1373,11 +1313,6 @@
"json-rpc-engine": true "json-rpc-engine": true
} }
}, },
"@metamask/permission-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/permission-controller>nanoid": { "@metamask/permission-controller>nanoid": {
"globals": { "globals": {
"crypto.getRandomValues": true "crypto.getRandomValues": true
@ -1389,46 +1324,11 @@
}, },
"packages": { "packages": {
"@metamask/base-controller": true, "@metamask/base-controller": true,
"@metamask/phishing-controller>@metamask/controller-utils": true, "@metamask/controller-utils": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/phishing-warning>eth-phishing-detect": true, "@metamask/phishing-warning>eth-phishing-detect": true,
"punycode": 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": { "@metamask/phishing-warning>eth-phishing-detect": {
"packages": { "packages": {
"eslint>optionator>fast-levenshtein": true "eslint>optionator>fast-levenshtein": true
@ -1491,14 +1391,19 @@
"@ethersproject/abi>@ethersproject/bytes": true, "@ethersproject/abi>@ethersproject/bytes": true,
"@ethersproject/bignumber": true, "@ethersproject/bignumber": true,
"@ethersproject/providers": true, "@ethersproject/providers": true,
"@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/base-controller": true,
"@metamask/phishing-controller>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>@metamask/controller-utils": true,
"@metamask/smart-transactions-controller>bignumber.js": true, "@metamask/smart-transactions-controller>bignumber.js": true,
"@metamask/smart-transactions-controller>isomorphic-fetch": true,
"fast-json-patch": true, "fast-json-patch": true,
"lodash": true "lodash": true
} }
}, },
"@metamask/smart-transactions-controller>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/smart-transactions-controller>@metamask/controller-utils": { "@metamask/smart-transactions-controller>@metamask/controller-utils": {
"globals": { "globals": {
"console.error": true, "console.error": true,
@ -1506,7 +1411,7 @@
"setTimeout": true "setTimeout": true
}, },
"packages": { "packages": {
"@metamask/phishing-controller>isomorphic-fetch": true, "@metamask/smart-transactions-controller>isomorphic-fetch": true,
"browserify>buffer": true, "browserify>buffer": true,
"eslint>fast-deep-equal": true, "eslint>fast-deep-equal": true,
"eth-ens-namehash": true, "eth-ens-namehash": true,
@ -1525,6 +1430,25 @@
"define": true "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": { "@metamask/snaps-controllers>nanoid": {
"globals": { "globals": {
"crypto.getRandomValues": true "crypto.getRandomValues": true
@ -2560,7 +2484,7 @@
}, },
"browserify>has": { "browserify>has": {
"packages": { "packages": {
"mocha>object.assign>function-bind": true "browserify>has>function-bind": true
} }
}, },
"browserify>os-browserify": { "browserify>os-browserify": {
@ -3615,7 +3539,7 @@
"globalthis>define-properties": { "globalthis>define-properties": {
"packages": { "packages": {
"globalthis>define-properties>has-property-descriptors": true, "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": { "globalthis>define-properties>has-property-descriptors": {
@ -4235,7 +4159,7 @@
}, },
"string.prototype.matchall>call-bind": { "string.prototype.matchall>call-bind": {
"packages": { "packages": {
"mocha>object.assign>function-bind": true, "browserify>has>function-bind": true,
"string.prototype.matchall>get-intrinsic": true "string.prototype.matchall>get-intrinsic": true
} }
}, },
@ -4247,7 +4171,7 @@
}, },
"packages": { "packages": {
"browserify>has": true, "browserify>has": true,
"mocha>object.assign>function-bind": true, "browserify>has>function-bind": true,
"string.prototype.matchall>has-symbols": true "string.prototype.matchall>has-symbols": true
} }
}, },

View File

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

View File

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

View File

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

View File

@ -1697,7 +1697,7 @@
}, },
"browserify>has": { "browserify>has": {
"packages": { "packages": {
"mocha>object.assign>function-bind": true "browserify>has>function-bind": true
} }
}, },
"browserify>insert-module-globals": { "browserify>insert-module-globals": {
@ -1885,12 +1885,7 @@
"process.platform": true "process.platform": true
}, },
"packages": { "packages": {
"chalk>supports-color>has-flag": true "sinon>supports-color>has-flag": true
}
},
"chalk>supports-color>has-flag": {
"globals": {
"process.argv": true
} }
}, },
"chokidar": { "chokidar": {
@ -2002,7 +1997,7 @@
"packages": { "packages": {
"cross-spawn>path-key": true, "cross-spawn>path-key": true,
"cross-spawn>shebang-command": true, "cross-spawn>shebang-command": true,
"cross-spawn>which": true "mocha>which": true
} }
}, },
"cross-spawn>path-key": { "cross-spawn>path-key": {
@ -2016,21 +2011,6 @@
"cross-spawn>shebang-command>shebang-regex": true "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": { "debounce-stream>duplexer": {
"builtin": { "builtin": {
"stream": true "stream": true
@ -2350,7 +2330,7 @@
"process": true "process": true
}, },
"packages": { "packages": {
"gulp-livereload>debug>ms": true, "mocha>ms": true,
"mocha>supports-color": true "mocha>supports-color": true
} }
}, },
@ -2471,7 +2451,7 @@
"process": true "process": true
}, },
"packages": { "packages": {
"gulp-livereload>debug>ms": true, "mocha>ms": true,
"mocha>supports-color": true "mocha>supports-color": true
} }
}, },
@ -2871,8 +2851,8 @@
"eslint>@eslint/eslintrc>globals": true, "eslint>@eslint/eslintrc>globals": true,
"eslint>ajv": true, "eslint>ajv": true,
"eslint>minimatch": true, "eslint>minimatch": true,
"eslint>strip-json-comments": true,
"globby>ignore": true, "globby>ignore": true,
"mocha>strip-json-comments": true,
"nock>debug": true "nock>debug": true
} }
}, },
@ -3094,9 +3074,9 @@
"process.cwd": true "process.cwd": true
}, },
"packages": { "packages": {
"chokidar>glob-parent": true,
"fast-glob>@nodelib/fs.stat": true, "fast-glob>@nodelib/fs.stat": true,
"fast-glob>@nodelib/fs.walk": true, "fast-glob>@nodelib/fs.walk": true,
"fast-glob>glob-parent": true,
"globby>merge2": true, "globby>merge2": true,
"stylelint>micromatch": true "stylelint>micromatch": true
} }
@ -3151,6 +3131,15 @@
"fast-glob>@nodelib/fs.walk>fastq>reusify": true "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": { "fs-extra": {
"builtin": { "builtin": {
"assert": true, "assert": true,
@ -3200,7 +3189,7 @@
"globalthis>define-properties": { "globalthis>define-properties": {
"packages": { "packages": {
"globalthis>define-properties>has-property-descriptors": true, "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": { "globalthis>define-properties>has-property-descriptors": {
@ -3370,8 +3359,8 @@
}, },
"packages": { "packages": {
"gulp-dart-sass>chalk>ansi-styles": true, "gulp-dart-sass>chalk>ansi-styles": true,
"gulp-dart-sass>chalk>supports-color": true, "gulp-dart-sass>chalk>escape-string-regexp": true,
"mocha>escape-string-regexp": true "gulp-dart-sass>chalk>supports-color": true
} }
}, },
"gulp-dart-sass>chalk>ansi-styles": { "gulp-dart-sass>chalk>ansi-styles": {
@ -3391,7 +3380,12 @@
"process.versions.node.split": true "process.versions.node.split": true
}, },
"packages": { "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": { "gulp-dart-sass>strip-ansi": {
@ -3430,9 +3424,9 @@
"process.platform": true "process.platform": true
}, },
"packages": { "packages": {
"gulp-dart-sass>chalk>escape-string-regexp": true,
"gulp-livereload>chalk>ansi-styles": true, "gulp-livereload>chalk>ansi-styles": true,
"gulp-livereload>chalk>supports-color": true, "gulp-livereload>chalk>supports-color": true
"mocha>escape-string-regexp": true
} }
}, },
"gulp-livereload>chalk>ansi-styles": { "gulp-livereload>chalk>ansi-styles": {
@ -3452,7 +3446,12 @@
"process.versions.node.split": true "process.versions.node.split": true
}, },
"packages": { "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": { "gulp-livereload>debug": {
@ -3469,7 +3468,7 @@
}, },
"packages": { "packages": {
"gulp-livereload>chalk>supports-color": true, "gulp-livereload>chalk>supports-color": true,
"gulp-livereload>debug>ms": true "mocha>ms": true
} }
}, },
"gulp-livereload>event-stream": { "gulp-livereload>event-stream": {
@ -3595,7 +3594,7 @@
"process": true "process": true
}, },
"packages": { "packages": {
"gulp-livereload>debug>ms": true, "mocha>ms": true,
"mocha>supports-color": true "mocha>supports-color": true
} }
}, },
@ -3699,9 +3698,9 @@
"process.platform": true "process.platform": true
}, },
"packages": { "packages": {
"gulp-dart-sass>chalk>escape-string-regexp": true,
"gulp-rtlcss>rtlcss>chalk>ansi-styles": true, "gulp-rtlcss>rtlcss>chalk>ansi-styles": true,
"gulp-rtlcss>rtlcss>chalk>supports-color": true, "gulp-rtlcss>rtlcss>chalk>supports-color": true
"mocha>escape-string-regexp": true
} }
}, },
"gulp-rtlcss>rtlcss>chalk>ansi-styles": { "gulp-rtlcss>rtlcss>chalk>ansi-styles": {
@ -3721,7 +3720,12 @@
"process.versions.node.split": true "process.versions.node.split": true
}, },
"packages": { "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": { "gulp-rtlcss>rtlcss>postcss": {
@ -3947,7 +3951,7 @@
"process": true "process": true
}, },
"packages": { "packages": {
"gulp-livereload>debug>ms": true, "mocha>ms": true,
"mocha>supports-color": true "mocha>supports-color": true
} }
}, },
@ -5204,11 +5208,11 @@
"chokidar>normalize-path": true, "chokidar>normalize-path": true,
"eslint>is-glob": true, "eslint>is-glob": true,
"gulp-watch>chokidar>async-each": true, "gulp-watch>chokidar>async-each": true,
"gulp-watch>glob-parent": true,
"gulp-watch>path-is-absolute": true, "gulp-watch>path-is-absolute": true,
"gulp>glob-watcher>anymatch": true, "gulp>glob-watcher>anymatch": true,
"gulp>glob-watcher>chokidar>braces": true, "gulp>glob-watcher>chokidar>braces": true,
"gulp>glob-watcher>chokidar>fsevents": 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>is-binary-path": true,
"gulp>glob-watcher>chokidar>readdirp": true, "gulp>glob-watcher>chokidar>readdirp": true,
"gulp>glob-watcher>chokidar>upath": true, "gulp>glob-watcher>chokidar>upath": true,
@ -5274,21 +5278,6 @@
"gulp-watch>chokidar>fsevents>node-pre-gyp": true "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": { "gulp>glob-watcher>chokidar>is-binary-path": {
"builtin": { "builtin": {
"path.extname": true "path.extname": true
@ -5663,8 +5652,8 @@
"process.nextTick": true "process.nextTick": true
}, },
"packages": { "packages": {
"gulp-watch>glob-parent": true,
"gulp>glob-watcher>is-negated-glob": 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>ordered-read-streams": true,
"gulp>vinyl-fs>glob-stream>pumpify": true, "gulp>vinyl-fs>glob-stream>pumpify": true,
"gulp>vinyl-fs>glob-stream>to-absolute-glob": true, "gulp>vinyl-fs>glob-stream>to-absolute-glob": true,
@ -5675,21 +5664,6 @@
"vinyl>remove-trailing-separator": true "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": { "gulp>vinyl-fs>glob-stream>ordered-read-streams": {
"builtin": { "builtin": {
"util.inherits": true "util.inherits": true
@ -5791,7 +5765,7 @@
"gulp>vinyl-fs>object.assign": { "gulp>vinyl-fs>object.assign": {
"packages": { "packages": {
"globalthis>define-properties": true, "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>call-bind": true,
"string.prototype.matchall>has-symbols": true "string.prototype.matchall>has-symbols": true
} }
@ -6092,9 +6066,9 @@
"process.platform": true "process.platform": true
}, },
"packages": { "packages": {
"gulp-dart-sass>chalk>escape-string-regexp": true,
"lavamoat>@babel/highlight>chalk>ansi-styles": true, "lavamoat>@babel/highlight>chalk>ansi-styles": true,
"lavamoat>@babel/highlight>chalk>supports-color": true, "lavamoat>@babel/highlight>chalk>supports-color": true
"mocha>escape-string-regexp": true
} }
}, },
"lavamoat>@babel/highlight>chalk>ansi-styles": { "lavamoat>@babel/highlight>chalk>ansi-styles": {
@ -6114,7 +6088,12 @@
"process.versions.node.split": true "process.versions.node.split": true
}, },
"packages": { "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": { "lavamoat>@lavamoat/aa": {
@ -6272,6 +6251,31 @@
"process.platform": true "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": { "mocha>minimatch>brace-expansion": {
"packages": { "packages": {
"mocha>minimatch>brace-expansion>concat-map": true, "mocha>minimatch>brace-expansion>concat-map": true,
@ -6280,22 +6284,15 @@
}, },
"mocha>supports-color": { "mocha>supports-color": {
"builtin": { "builtin": {
"os.release": true "os.release": true,
"tty.isatty": true
}, },
"globals": { "globals": {
"process.env": true, "process.env": true,
"process.platform": true, "process.platform": true
"process.stderr": true,
"process.stdout": true,
"process.versions.node.split": true
}, },
"packages": { "packages": {
"mocha>supports-color>has-flag": true "sinon>supports-color>has-flag": true
}
},
"mocha>supports-color>has-flag": {
"globals": {
"process.argv": true
} }
}, },
"mocha>which": { "mocha>which": {
@ -6565,9 +6562,9 @@
"react-markdown>unified": { "react-markdown>unified": {
"packages": { "packages": {
"jsdom>request>extend": true, "jsdom>request>extend": true,
"mocha>yargs-unparser>is-plain-obj": true,
"react-markdown>unified>bail": true, "react-markdown>unified>bail": true,
"react-markdown>unified>is-buffer": true, "react-markdown>unified>is-buffer": true,
"react-markdown>unified>is-plain-obj": true,
"react-markdown>unified>trough": true, "react-markdown>unified>trough": true,
"react-markdown>vfile": true "react-markdown>vfile": true
} }
@ -6767,6 +6764,11 @@
"process.platform": true "process.platform": true
} }
}, },
"sinon>supports-color>has-flag": {
"globals": {
"process.argv": true
}
},
"source-map": { "source-map": {
"builtin": { "builtin": {
"fs.readFile": true, "fs.readFile": true,
@ -6801,7 +6803,7 @@
}, },
"string.prototype.matchall>call-bind": { "string.prototype.matchall>call-bind": {
"packages": { "packages": {
"mocha>object.assign>function-bind": true, "browserify>has>function-bind": true,
"string.prototype.matchall>get-intrinsic": true "string.prototype.matchall>get-intrinsic": true
} }
}, },
@ -6840,7 +6842,7 @@
}, },
"packages": { "packages": {
"browserify>has": true, "browserify>has": true,
"mocha>object.assign>function-bind": true, "browserify>has>function-bind": true,
"string.prototype.matchall>has-symbols": true "string.prototype.matchall>has-symbols": true
} }
}, },
@ -6896,6 +6898,7 @@
"globby>ignore": true, "globby>ignore": true,
"globby>slash": true, "globby>slash": true,
"lodash": true, "lodash": true,
"mocha>log-symbols": true,
"nock>debug": true, "nock>debug": true,
"nyc>resolve-from": true, "nyc>resolve-from": true,
"stylelint>@stylelint/postcss-css-in-js": true, "stylelint>@stylelint/postcss-css-in-js": true,
@ -6912,7 +6915,6 @@
"stylelint>import-lazy": true, "stylelint>import-lazy": true,
"stylelint>known-css-properties": true, "stylelint>known-css-properties": true,
"stylelint>leven": true, "stylelint>leven": true,
"stylelint>log-symbols": true,
"stylelint>mathml-tag-names": true, "stylelint>mathml-tag-names": true,
"stylelint>micromatch": true, "stylelint>micromatch": true,
"stylelint>normalize-selector": true, "stylelint>normalize-selector": true,
@ -7094,12 +7096,7 @@
"process.platform": true "process.platform": true
}, },
"packages": { "packages": {
"stylelint>chalk>supports-color>has-flag": true "sinon>supports-color>has-flag": true
}
},
"stylelint>chalk>supports-color>has-flag": {
"globals": {
"process.argv": true
} }
}, },
"stylelint>cosmiconfig": { "stylelint>cosmiconfig": {
@ -7212,8 +7209,8 @@
"process.platform": true "process.platform": true
}, },
"packages": { "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": { "stylelint>global-modules>global-prefix>ini": {
@ -7221,39 +7218,24 @@
"process": true "process": true
} }
}, },
"stylelint>globjoin": { "stylelint>global-modules>global-prefix>which": {
"builtin": { "builtin": {
"path.join": true "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": { "globals": {
"process.env": true, "process.cwd": true,
"process.env.OSTYPE": true,
"process.env.PATH": true,
"process.env.PATHEXT": true,
"process.platform": true "process.platform": true
}, },
"packages": { "packages": {
"stylelint>log-symbols>chalk>supports-color>has-flag": true "mocha>which>isexe": true
} }
}, },
"stylelint>log-symbols>chalk>supports-color>has-flag": { "stylelint>globjoin": {
"globals": { "builtin": {
"process.argv": true "path.join": true
} }
}, },
"stylelint>micromatch": { "stylelint>micromatch": {
@ -7557,9 +7539,9 @@
}, },
"stylelint>table>slice-ansi": { "stylelint>table>slice-ansi": {
"packages": { "packages": {
"mocha>yargs>string-width>is-fullwidth-code-point": true,
"stylelint>table>slice-ansi>ansi-styles": 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": { "stylelint>table>slice-ansi>ansi-styles": {
@ -7569,7 +7551,7 @@
}, },
"stylelint>table>string-width": { "stylelint>table>string-width": {
"packages": { "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>emoji-regex": true,
"stylelint>table>string-width>strip-ansi": true "stylelint>table>string-width>strip-ansi": true
} }

View File

@ -226,11 +226,11 @@
"@keystonehq/metamask-airgapped-keyring": "^0.6.1", "@keystonehq/metamask-airgapped-keyring": "^0.6.1",
"@lavamoat/snow": "^1.5.0", "@lavamoat/snow": "^1.5.0",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@metamask/address-book-controller": "^1.0.0", "@metamask/address-book-controller": "^2.0.0",
"@metamask/announcement-controller": "^2.0.1", "@metamask/announcement-controller": "^3.0.0",
"@metamask/approval-controller": "^1.0.0", "@metamask/approval-controller": "^2.1.0",
"@metamask/assets-controllers": "^4.0.1", "@metamask/assets-controllers": "^5.0.0",
"@metamask/base-controller": "^1.0.0", "@metamask/base-controller": "^2.0.0",
"@metamask/contract-metadata": "^2.3.1", "@metamask/contract-metadata": "^2.3.1",
"@metamask/controller-utils": "^3.1.0", "@metamask/controller-utils": "^3.1.0",
"@metamask/design-tokens": "^1.9.0", "@metamask/design-tokens": "^1.9.0",
@ -242,19 +242,19 @@
"@metamask/eth-ledger-bridge-keyring": "^0.13.0", "@metamask/eth-ledger-bridge-keyring": "^0.13.0",
"@metamask/eth-token-tracker": "^4.0.0", "@metamask/eth-token-tracker": "^4.0.0",
"@metamask/etherscan-link": "^2.2.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/jazzicon": "^2.0.0",
"@metamask/key-tree": "^7.0.0", "@metamask/key-tree": "^7.0.0",
"@metamask/logo": "^3.1.1", "@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/metamask-eth-abis": "^3.0.0",
"@metamask/notification-controller": "^1.0.0", "@metamask/notification-controller": "^2.0.0",
"@metamask/obs-store": "^8.0.0", "@metamask/obs-store": "^8.1.0",
"@metamask/permission-controller": "^3.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/post-message-stream": "^6.0.0",
"@metamask/providers": "^10.2.1", "@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/rpc-methods": "^0.32.2",
"@metamask/safe-event-emitter": "^2.0.0", "@metamask/safe-event-emitter": "^2.0.0",
"@metamask/scure-bip39": "^2.0.3", "@metamask/scure-bip39": "^2.0.3",
@ -263,7 +263,7 @@
"@metamask/snaps-controllers": "^0.32.2", "@metamask/snaps-controllers": "^0.32.2",
"@metamask/snaps-ui": "^0.32.2", "@metamask/snaps-ui": "^0.32.2",
"@metamask/snaps-utils": "^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/swappable-obj-proxy": "^2.1.0",
"@metamask/utils": "^5.0.0", "@metamask/utils": "^5.0.0",
"@ngraveio/bc-ur": "^1.1.6", "@ngraveio/bc-ur": "^1.1.6",
@ -368,6 +368,7 @@
"@babel/preset-typescript": "^7.16.7", "@babel/preset-typescript": "^7.16.7",
"@babel/register": "^7.5.5", "@babel/register": "^7.5.5",
"@ethersproject/bignumber": "^5.7.0", "@ethersproject/bignumber": "^5.7.0",
"@json-rpc-specification/meta-schema": "^1.0.6",
"@lavamoat/allow-scripts": "^2.0.3", "@lavamoat/allow-scripts": "^2.0.3",
"@lavamoat/lavapack": "^5.0.0", "@lavamoat/lavapack": "^5.0.0",
"@metamask/auto-changelog": "^2.1.0", "@metamask/auto-changelog": "^2.1.0",
@ -419,6 +420,7 @@
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-redux": "^7.1.25", "@types/react-redux": "^7.1.25",
"@types/remote-redux-devtools": "^0.5.5", "@types/remote-redux-devtools": "^0.5.5",
"@types/sinon": "^10.0.13",
"@types/w3c-web-hid": "^1.0.3", "@types/w3c-web-hid": "^1.0.3",
"@types/watchify": "^3.11.1", "@types/watchify": "^3.11.1",
"@types/yargs": "^17.0.8", "@types/yargs": "^17.0.8",
@ -451,7 +453,7 @@
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^26.6.0", "eslint-plugin-jest": "^26.6.0",
"eslint-plugin-jsdoc": "^39.3.3", "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-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.23.1", "eslint-plugin-react": "^7.23.1",
@ -495,7 +497,7 @@
"lockfile-lint": "^4.9.6", "lockfile-lint": "^4.9.6",
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"madge": "^5.0.1", "madge": "^5.0.1",
"mocha": "^7.2.0", "mocha": "^9.2.2",
"mockttp": "^2.6.0", "mockttp": "^2.6.0",
"nock": "^13.2.9", "nock": "^13.2.9",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",

View File

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

View File

@ -510,6 +510,8 @@ export enum MetaMetricsEventName {
SignatureFailed = 'Signature Failed', SignatureFailed = 'Signature Failed',
SignatureRejected = 'Signature Rejected', SignatureRejected = 'Signature Rejected',
SignatureRequested = 'Signature Requested', SignatureRequested = 'Signature Requested',
TermsOfUseShown = 'Terms of Use Shown',
TermsOfUseAccepted = 'Terms of Use Accepted',
TokenImportButtonClicked = 'Import Token Button Clicked', TokenImportButtonClicked = 'Import Token Button Clicked',
TokenScreenOpened = 'Token Screen Opened', TokenScreenOpened = 'Token Screen Opened',
SupportLinkClicked = 'Support Link Clicked', SupportLinkClicked = 'Support Link Clicked',

View File

@ -238,7 +238,7 @@ export const INFURA_PROVIDER_TYPES = [
NETWORK_TYPES.MAINNET, NETWORK_TYPES.MAINNET,
NETWORK_TYPES.GOERLI, NETWORK_TYPES.GOERLI,
NETWORK_TYPES.SEPOLIA, NETWORK_TYPES.SEPOLIA,
]; ] as const;
export const TEST_CHAINS = [ export const TEST_CHAINS = [
CHAIN_IDS.GOERLI, 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": { "DNS": {
"resolution": "" "resolution": ""
}, },
"activeTab": {
"id": 113,
"title": "E2E Test Dapp",
"origin": "https://metamask.github.io",
"protocol": "https:",
"url": "https://metamask.github.io/test-dapp/"
},
"appState": { "appState": {
"networkDropdownOpen": false, "networkDropdownOpen": false,
"gasIsLoading": false, "gasIsLoading": false,
@ -16,7 +23,8 @@
"name": null "name": null
} }
}, },
"warning": null "warning": null,
"alertOpen": false
}, },
"confirmTransaction": { "confirmTransaction": {
"txData": { "txData": {
@ -42,6 +50,10 @@
"history": { "history": {
"mostRecentOverviewPage": "/mostRecentOverviewPage" "mostRecentOverviewPage": "/mostRecentOverviewPage"
}, },
"invalidCustomNetwork": {
"state": "CLOSED",
"networkName": ""
},
"metamask": { "metamask": {
"ipfsGateway": "", "ipfsGateway": "",
"dismissSeedBackUpReminder": false, "dismissSeedBackUpReminder": false,
@ -88,6 +100,14 @@
"ensResolutionsByAddress": {}, "ensResolutionsByAddress": {},
"isAccountMenuOpen": false, "isAccountMenuOpen": false,
"isUnlocked": true, "isUnlocked": true,
"completedOnboarding": true,
"usedNetworks": {
"0x1": true,
"0x5": true,
"0x539": true
},
"showTestnetMessageInDropdown": true,
"networkConfigurations": {},
"alertEnabledness": { "alertEnabledness": {
"unconnectedAccount": true "unconnectedAccount": true
}, },
@ -1358,5 +1378,8 @@
"balance": "0x4563918244f40000" "balance": "0x4563918244f40000"
}, },
"stage": "DRAFT" "stage": "DRAFT"
},
"unconnectedAccount": {
"state": "CLOSED"
} }
} }

View File

@ -1535,7 +1535,18 @@
"origin": "tmashuang.github.io" "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": { "send": {
"amountMode": "INPUT", "amountMode": "INPUT",

View File

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

View File

@ -222,6 +222,9 @@ const getWindowHandles = async (driver, handlesCount) => {
}; };
const importSRPOnboardingFlow = async (driver, seedPhrase, password) => { const importSRPOnboardingFlow = async (driver, seedPhrase, password) => {
// agree to terms of use
await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
// welcome // welcome
await driver.clickElement('[data-testid="onboarding-import-wallet"]'); await driver.clickElement('[data-testid="onboarding-import-wallet"]');
@ -262,6 +265,9 @@ const completeImportSRPOnboardingFlowWordByWord = async (
seedPhrase, seedPhrase,
password, password,
) => { ) => {
// agree to terms of use
await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
// welcome // welcome
await driver.clickElement('[data-testid="onboarding-import-wallet"]'); await driver.clickElement('[data-testid="onboarding-import-wallet"]');
@ -293,6 +299,9 @@ const completeImportSRPOnboardingFlowWordByWord = async (
}; };
const completeCreateNewWalletOnboardingFlow = async (driver, password) => { const completeCreateNewWalletOnboardingFlow = async (driver, password) => {
// agree to terms of use
await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
// welcome // welcome
await driver.clickElement('[data-testid="onboarding-create-wallet"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]');
@ -342,6 +351,9 @@ const completeCreateNewWalletOnboardingFlow = async (driver, password) => {
}; };
const importWrongSRPOnboardingFlow = async (driver, seedPhrase) => { const importWrongSRPOnboardingFlow = async (driver, seedPhrase) => {
// agree to terms of use
await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
// welcome // welcome
await driver.clickElement('[data-testid="onboarding-import-wallet"]'); 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 () { describe('Going through the first time flow', function () {
it('clicks the "Create New Wallet" button on the welcome screen', async 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"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]');
}); });

View File

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

View File

@ -392,7 +392,9 @@ describe('Custom network', function () {
text: 'Delete', 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 // should be deleted from the modal shown again to complete deletion custom network
await driver.clickElement({ await driver.clickElement({
tag: 'button', tag: 'button',

View File

@ -119,16 +119,16 @@ describe('Create token, approve token and approve token without gas', function (
); );
await driver.clickElement({ await driver.clickElement({
text: 'Verify contract details', text: 'Verify third-party details',
css: '.token-allowance-container__verify-link', css: '.token-allowance-container__verify-link',
}); });
const modalTitle = await driver.waitForSelector({ const modalTitle = await driver.waitForSelector({
text: 'Contract details', text: 'Third-party details',
tag: 'h5', tag: 'h5',
}); });
assert.equal(await modalTitle.getText(), 'Contract details'); assert.equal(await modalTitle.getText(), 'Third-party details');
await driver.clickElement({ await driver.clickElement({
text: 'Got it', text: 'Got it',
@ -292,10 +292,13 @@ describe('Create token, approve token and approve token without gas', function (
await gasLimitInput.fill('60001'); await gasLimitInput.fill('60001');
await driver.clickElement({ text: 'Save', tag: 'button' }); await driver.clickElement({ text: 'Save', tag: 'button' });
await driver.waitForSelector({ await driver.waitForSelector(
css: '.box--flex-direction-row > h6', {
text: '0.0006 ETH', css: '.box--flex-direction-row > h6',
}); text: '0.0006 ETH',
},
{ timeout: 15000 },
);
// editing spending cap // editing spending cap
await driver.clickElement({ await driver.clickElement({

View File

@ -29,6 +29,8 @@ describe('Incremental Security', function () {
}, },
async ({ driver }) => { async ({ driver }) => {
await driver.navigate(); await driver.navigate();
// agree to terms of use
await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
// welcome // welcome
await driver.clickElement('[data-testid="onboarding-create-wallet"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]');

View File

@ -15,6 +15,8 @@ describe('MetaMask Responsive UI', function () {
}, },
async ({ driver }) => { async ({ driver }) => {
await driver.navigate(); await driver.navigate();
// agree to terms of use
await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
// welcome // welcome
await driver.clickElement('[data-testid="onboarding-create-wallet"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]');

View File

@ -108,6 +108,9 @@ describe('MetaMask onboarding', function () {
async ({ driver }) => { async ({ driver }) => {
await driver.navigate(); await driver.navigate();
// accept terms of use
await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
// welcome // welcome
await driver.clickElement('[data-testid="onboarding-import-wallet"]'); await driver.clickElement('[data-testid="onboarding-import-wallet"]');
@ -145,6 +148,7 @@ describe('MetaMask onboarding', function () {
async ({ driver }) => { async ({ driver }) => {
await driver.navigate(); await driver.navigate();
await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
await driver.clickElement('[data-testid="onboarding-create-wallet"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]');
// metrics // metrics
@ -208,6 +212,7 @@ describe('MetaMask onboarding', function () {
async ({ driver }) => { async ({ driver }) => {
await driver.navigate(); await driver.navigate();
await driver.clickElement('[data-testid="onboarding-terms-checkbox"]');
await driver.clickElement('[data-testid="onboarding-create-wallet"]'); await driver.clickElement('[data-testid="onboarding-create-wallet"]');
// metrics // metrics

View File

@ -354,9 +354,12 @@ describe('Send ETH from dapp using advanced gas controls', function () {
'.transaction-list-item__primary-currency', '.transaction-list-item__primary-currency',
); );
await txValue.click(); await txValue.click();
const baseFeeValue = await driver.waitForSelector({ const baseFeeValue = await driver.waitForSelector(
text: '0.000000025', {
}); text: '0.000000025',
},
{ timeout: 15000 },
);
assert.equal(await baseFeeValue.getText(), '0.000000025'); 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'); assert.equal(await origin.getText(), 'http://127.0.0.1:8080');
verifyContractDetailsButton.click(); 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.findElement('[data-testid="recipient"]');
await driver.clickElement({ text: 'Got it', tag: 'button' }); 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'); assert.equal(await origin.getText(), 'http://127.0.0.1:8080');
verifyContractDetailsButton.click(); 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.findElement('[data-testid="recipient"]');
await driver.clickElement({ text: 'Got it', tag: 'button' }); await driver.clickElement({ text: 'Got it', tag: 'button' });

View File

@ -5,5 +5,11 @@ declare module '@metamask/eth-keyring-controller' {
signPersonalMessage: (...any) => any; signPersonalMessage: (...any) => any;
signTypedMessage: (...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 'transaction-status-label/index';
@import 'wallet-overview/index'; @import 'wallet-overview/index';
@import 'whats-new-popup/index'; @import 'whats-new-popup/index';
@import 'terms-of-use-popup/index';
@import 'loading-network-screen/index'; @import 'loading-network-screen/index';
@import 'transaction-decoding/index'; @import 'transaction-decoding/index';
@import 'advanced-gas-fee-popover/index'; @import 'advanced-gas-fee-popover/index';

View File

@ -89,6 +89,7 @@ export default class AppHeader extends PureComponent {
className={classnames('account-menu__icon', { className={classnames('account-menu__icon', {
'account-menu__icon--disabled': disabled, 'account-menu__icon--disabled': disabled,
})} })}
disabled={Boolean(disabled)}
onClick={() => { onClick={() => {
if (!disabled) { if (!disabled) {
!isAccountMenuOpen && !isAccountMenuOpen &&

View File

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

View File

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { Menu } from '../../../ui/menu'; import { Menu } from '../../../ui/menu';
import { ICON_NAMES } from '../../../component-library/icon/deprecated'; 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'; import { useI18nContext } from '../../../../hooks/useI18nContext';
const ConnectedAccountsListOptions = ({ 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 { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
@ -41,6 +41,7 @@ export default function CustomSpendingCap({
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
const inputRef = useRef(null);
const value = useSelector(getCustomTokenAmount); const value = useSelector(getCustomTokenAmount);
@ -139,6 +140,15 @@ export default function CustomSpendingCap({
passTheErrorText(error); passTheErrorText(error);
}, [error, passTheErrorText]); }, [error, passTheErrorText]);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus({
preventScroll: true,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputRef.current]);
const chooseTooltipContentText = decConversionGreaterThan( const chooseTooltipContentText = decConversionGreaterThan(
value, value,
currentTokenBalance, currentTokenBalance,
@ -182,8 +192,8 @@ export default function CustomSpendingCap({
} }
> >
<FormField <FormField
inputRef={inputRef}
dataTestId="custom-spending-cap-input" dataTestId="custom-spending-cap-input"
autoFocus
wrappingLabelProps={{ as: 'div' }} wrappingLabelProps={{ as: 'div' }}
id={ id={
decConversionGreaterThan(value, currentTokenBalance) decConversionGreaterThan(value, currentTokenBalance)

View File

@ -20,7 +20,8 @@ import {
getSnapName, getSnapName,
removeSnapIdPrefix, removeSnapIdPrefix,
} from '../../../../helpers/utils/util'; } from '../../../../helpers/utils/util';
import { Text, ButtonIcon } from '../../../component-library'; import { ButtonIcon } from '../../../component-library/button-icon/deprecated';
import { Text } from '../../../component-library';
import { import {
ICON_NAMES, ICON_NAMES,
ICON_SIZES, 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 React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { object } from '@storybook/addon-knobs';
import { panel, text, heading, divider, copyable } from '@metamask/snaps-ui'; import { panel, text, heading, divider, copyable } from '@metamask/snaps-ui';
import configureStore from '../../../../store/store'; import configureStore from '../../../../store/store';
import testData from '../../../../../.storybook/test-data'; import testData from '../../../../../.storybook/test-data';
@ -10,8 +9,13 @@ const store = configureStore(testData);
export default { export default {
title: 'Components/App/SnapUIRenderer', title: 'Components/App/SnapUIRenderer',
component: SnapUIRenderer,
decorators: [(story) => <Provider store={store}>{story()}</Provider>], decorators: [(story) => <Provider store={store}>{story()}</Provider>],
argTypes: {
data: {
control: 'object',
},
},
}; };
const DATA = panel([ const DATA = panel([
@ -22,13 +26,18 @@ const DATA = panel([
copyable('Text you can copy'), copyable('Text you can copy'),
]); ]);
export const DefaultStory = () => ( export const DefaultStory = (args) => (
<SnapUIRenderer <SnapUIRenderer snapId="local:http://localhost:8080/" data={args.data} />
snapId="local:http://localhost:8080/"
data={object('data', DATA)}
/>
); );
export const ErrorStory = () => ( DefaultStory.args = {
<SnapUIRenderer snapId="local:http://localhost:8080/" data="foo" /> 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} onMouseDown={onMouseDown}
onMouseUp={onMouseUp} onMouseUp={onMouseUp}
className="hold-to-reveal-button__button-hold" 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()} {renderPreCompleteContent()}
{renderPostCompleteContent()} {renderPostCompleteContent()}
</Box> </Box>

View File

@ -14,9 +14,8 @@ import { CONNECTED_ACCOUNTS_ROUTE } from '../../../helpers/constants/routes';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { getOriginOfCurrentTab } from '../../../selectors'; import { getOriginOfCurrentTab } from '../../../selectors';
import { MetaMetricsContext } from '../../../contexts/metametrics'; 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 { ICON_NAMES } from '../../component-library/icon/deprecated';
import { GlobalMenu } from '../../multichain/global-menu';
import AccountOptionsMenu from './account-options-menu'; import AccountOptionsMenu from './account-options-menu';
export default function MenuBar() { export default function MenuBar() {
@ -34,7 +33,7 @@ export default function MenuBar() {
return ( return (
<div className="menu-bar"> <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 <ConnectedStatusIndicator
onClick={() => history.push(CONNECTED_ACCOUNTS_ROUTE)} onClick={() => history.push(CONNECTED_ACCOUNTS_ROUTE)}
/> />
@ -58,18 +57,12 @@ export default function MenuBar() {
}} }}
/> />
</span> </span>
{accountOptionsMenuOpen && {accountOptionsMenuOpen && (
(process.env.MULTICHAIN ? ( <AccountOptionsMenu
<GlobalMenu anchorElement={ref.current}
anchorElement={ref.current} onClose={() => setAccountOptionsMenuOpen(false)}
closeMenu={() => setAccountOptionsMenuOpen(false)} />
/> )}
) : (
<AccountOptionsMenu
anchorElement={ref.current}
onClose={() => setAccountOptionsMenuOpen(false)}
/>
))}
</div> </div>
); );
} }

View File

@ -25,7 +25,7 @@ import { useCopyToClipboard } from '../../../../hooks/useCopyToClipboard';
import { getAddressBookEntry } from '../../../../selectors'; import { getAddressBookEntry } from '../../../../selectors';
import { TokenStandard } from '../../../../../shared/constants/transaction'; import { TokenStandard } from '../../../../../shared/constants/transaction';
import NftCollectionImage from '../../../ui/nft-collection-image/nft-collection-image'; 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'; import { ICON_NAMES } from '../../../component-library/icon/deprecated';
export default function ContractDetailsModal({ 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 withModalProps from '../../../../helpers/higher-order-components/with-modal-props';
import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useI18nContext } from '../../../../hooks/useI18nContext';
import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url';
import { ButtonIcon } from '../../../component-library'; import { ButtonIcon } from '../../../component-library/button-icon/deprecated';
import { import {
ICON_NAMES, ICON_NAMES,
ICON_SIZES, ICON_SIZES,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,29 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getWeightedPermissions } from '../../../helpers/utils/permission'; import {
getRightIcon,
getWeightedPermissions,
} from '../../../helpers/utils/permission';
import { useI18nContext } from '../../../hooks/useI18nContext'; 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({ export default function PermissionsConnectPermissionList({
permissions, permissions,
@ -12,22 +32,11 @@ export default function PermissionsConnectPermissionList({
const t = useI18nContext(); const t = useI18nContext();
return ( return (
<Box paddingTop={2} paddingBottom={2}> <div className="permissions-connect-permission-list">
{getWeightedPermissions(t, permissions, targetSubjectMetadata).map( {getWeightedPermissions(t, permissions, targetSubjectMetadata).map(
(permission, index) => { getDescriptionNode,
return (
<PermissionCell
title={permission.label}
description={permission.description}
weight={permission.weight}
avatarIcon={permission.leftIcon}
dateApproved={permission?.permissionValue?.date}
key={`${permission.permissionName}-${index}`}
/>
);
},
)} )}
</Box> </div>
); );
} }

View File

@ -20,18 +20,5 @@ DefaultStory.storyName = 'Default';
DefaultStory.args = { DefaultStory.args = {
permissions: { permissions: {
eth_accounts: {}, 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 Checkbox from '../../ui/check-box';
import { I18nContext } from '../../../contexts/i18n'; import { I18nContext } from '../../../contexts/i18n';
import { PageContainerFooter } from '../../ui/page-container'; import { PageContainerFooter } from '../../ui/page-container';
import { isAddressLedger } from '../../../ducks/metamask/metamask';
import { import {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
getSubjectMetadata, getSubjectMetadata,
@ -20,6 +21,7 @@ import {
import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message'; 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 { 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 Header from './signature-request-siwe-header';
import Message from './signature-request-siwe-message'; import Message from './signature-request-siwe-message';
@ -39,6 +41,8 @@ export default function SignatureRequestSIWE({
}, },
} = txData; } = txData;
const isLedgerWallet = useSelector((state) => isAddressLedger(state, from));
const fromAccount = getAccountByAddress(allAccounts, from); const fromAccount = getAccountByAddress(allAccounts, from);
const targetSubjectMetadata = subjectMetadata[origin]; const targetSubjectMetadata = subjectMetadata[origin];
@ -115,6 +119,13 @@ export default function SignatureRequestSIWE({
])} ])}
</BannerAlert> </BannerAlert>
)} )}
{isLedgerWallet && (
<div className="confirm-approve-content__ledger-instruction-wrapper">
<LedgerInstructionField showDataInstruction />
</div>
)}
{!isSIWEDomainValid && ( {!isSIWEDomainValid && (
<BannerAlert <BannerAlert
severity={SEVERITIES.DANGER} 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 render = (txData = mockProps.txData) => {
const store = configureStore(mockStoreInitialState); const store = configureStore(mockStoreInitialState);
@ -110,4 +119,21 @@ describe('SignatureRequestSIWE (Sign in with Ethereum)', () => {
expect(bannerAlert).toBeTruthy(); expect(bannerAlert).toBeTruthy();
expect(bannerAlert).toHaveTextContent('Deceptive site request.'); 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 <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" 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> </h6>
</a> </a>
</div> </div>
@ -999,7 +999,7 @@ exports[`Signature Request Component render should match snapshot when we want t
<h6 <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" 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> </h6>
</a> </a>
</div> </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 React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; 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 WalletOverview = ({ balance, buttons, className, icon, loading }) => {
const selectedIdentity = useSelector(getSelectedIdentity);
const checksummedAddress = toChecksumHexAddress(selectedIdentity?.address);
return ( return (
<div className={classnames('wallet-overview', className)}> <div className={classnames('wallet-overview', className)}>
<div className="wallet-overview__balance"> <div className="wallet-overview__balance">
{loading ? null : icon} {process.env.MULTICHAIN ? (
<AddressCopyButton address={checksummedAddress} shorten />
) : (
<>{loading ? null : icon}</>
)}
{balance} {balance}
</div> </div>
<div className="wallet-overview__buttons">{buttons}</div> <div className="wallet-overview__buttons">{buttons}</div>

View File

@ -66,28 +66,6 @@
height: 1px; height: 1px;
width: 100%; 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 { .popover-wrap.whats-new-popup__popover {

View File

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

View File

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

View File

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