mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
Merge remote-tracking branch 'origin/master' into Version-v10.35.0
This commit is contained in:
commit
313deae6d1
5
.iyarc
5
.iyarc
@ -5,11 +5,6 @@ GHSA-257v-vj4p-3w2h
|
|||||||
# addressed by temporary patch in .yarn/patches/request-npm-2.88.2-f4a57c72c4.patch
|
# addressed by temporary patch in .yarn/patches/request-npm-2.88.2-f4a57c72c4.patch
|
||||||
GHSA-p8p7-x288-28g6
|
GHSA-p8p7-x288-28g6
|
||||||
|
|
||||||
# Prototype pollution
|
|
||||||
# Not easily patched
|
|
||||||
# Minimal risk to us because we're using lockdown which also prevents this case of prototype pollution
|
|
||||||
GHSA-h755-8qp9-cq85
|
|
||||||
|
|
||||||
# tough-cookie
|
# tough-cookie
|
||||||
# this will go away soon when we get rid of web3-provider-engine
|
# this will go away soon when we get rid of web3-provider-engine
|
||||||
GHSA-72xf-g2v4-qvf3
|
GHSA-72xf-g2v4-qvf3
|
||||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [10.35.0]
|
## [10.35.0]
|
||||||
### Uncategorized
|
### Uncategorized
|
||||||
- temp
|
- temp
|
||||||
|
## [10.34.5]
|
||||||
|
### Changed
|
||||||
|
- Improve error diagnostic information
|
||||||
|
- Add additional logging for state migrations ([#20424](https://github.com/MetaMask/metamask-extension/pull/20424), [#20517](https://github.com/MetaMask/metamask-extension/pull/20517), [#20521](https://github.com/MetaMask/metamask-extension/pull/20521))
|
||||||
|
- Improve diagnostic state snapshot ([#20457](https://github.com/MetaMask/metamask-extension/pull/20457), [#20491](https://github.com/MetaMask/metamask-extension/pull/20491), [#20428](https://github.com/MetaMask/metamask-extension/pull/20428), [#20458](https://github.com/MetaMask/metamask-extension/pull/20458))
|
||||||
|
- Capture additional errors when state migrations fail ([#20427](https://github.com/MetaMask/metamask-extension/pull/20427))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix bug where state was temporarily incomplete upon initial load ([#20468](https://github.com/MetaMask/metamask-extension/pull/20468))
|
||||||
|
- In rare circumstances, this bug may have resulted in data loss (of preferences, permissions, or tracked NFTs/tokens) or UI crashes.
|
||||||
|
|
||||||
## [10.34.4]
|
## [10.34.4]
|
||||||
### Changed
|
### Changed
|
||||||
- Updated snaps execution environment ([#20420](https://github.com/MetaMask/metamask-extension/pull/20420))
|
- Updated snaps execution environment ([#20420](https://github.com/MetaMask/metamask-extension/pull/20420))
|
||||||
@ -3891,6 +3902,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.35.0...HEAD
|
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.35.0...HEAD
|
||||||
[10.35.0]: https://github.com/MetaMask/metamask-extension/compare/v10.34.0...v10.35.0
|
[10.35.0]: https://github.com/MetaMask/metamask-extension/compare/v10.34.0...v10.35.0
|
||||||
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.34.4...HEAD
|
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.34.4...HEAD
|
||||||
|
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.34.5...HEAD
|
||||||
|
[10.34.5]: https://github.com/MetaMask/metamask-extension/compare/v10.34.4...v10.34.5
|
||||||
[10.34.4]: https://github.com/MetaMask/metamask-extension/compare/v10.34.3...v10.34.4
|
[10.34.4]: https://github.com/MetaMask/metamask-extension/compare/v10.34.3...v10.34.4
|
||||||
[10.34.3]: https://github.com/MetaMask/metamask-extension/compare/v10.34.2...v10.34.3
|
[10.34.3]: https://github.com/MetaMask/metamask-extension/compare/v10.34.2...v10.34.3
|
||||||
[10.34.2]: https://github.com/MetaMask/metamask-extension/compare/v10.34.1...v10.34.2
|
[10.34.2]: https://github.com/MetaMask/metamask-extension/compare/v10.34.1...v10.34.2
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
* @file The entry point for the web extension singleton process.
|
* @file The entry point for the web extension singleton process.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// This import sets up a global function required for Sentry to function.
|
// Disabled to allow setting up initial state hooks first
|
||||||
|
|
||||||
|
// This import sets up global functions required for Sentry to function.
|
||||||
// It must be run first in case an error is thrown later during initialization.
|
// It must be run first in case an error is thrown later during initialization.
|
||||||
import './lib/setup-persisted-state-hook';
|
import './lib/setup-initial-state-hooks';
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import endOfStream from 'end-of-stream';
|
import endOfStream from 'end-of-stream';
|
||||||
@ -13,6 +15,7 @@ import debounce from 'debounce-stream';
|
|||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import browser from 'webextension-polyfill';
|
import browser from 'webextension-polyfill';
|
||||||
import { storeAsStream } from '@metamask/obs-store';
|
import { storeAsStream } from '@metamask/obs-store';
|
||||||
|
import { isObject } from '@metamask/utils';
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||||
import { ApprovalType } from '@metamask/controller-utils';
|
import { ApprovalType } from '@metamask/controller-utils';
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
@ -41,7 +44,7 @@ import Migrator from './lib/migrator';
|
|||||||
import ExtensionPlatform from './platforms/extension';
|
import ExtensionPlatform from './platforms/extension';
|
||||||
import LocalStore from './lib/local-store';
|
import LocalStore from './lib/local-store';
|
||||||
import ReadOnlyNetworkStore from './lib/network-store';
|
import ReadOnlyNetworkStore from './lib/network-store';
|
||||||
import { SENTRY_STATE } from './lib/setupSentry';
|
import { SENTRY_BACKGROUND_STATE } from './lib/setupSentry';
|
||||||
|
|
||||||
import createStreamSink from './lib/createStreamSink';
|
import createStreamSink from './lib/createStreamSink';
|
||||||
import NotificationManager, {
|
import NotificationManager, {
|
||||||
@ -68,6 +71,12 @@ import DesktopManager from '@metamask/desktop/dist/desktop-manager';
|
|||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
/* eslint-enable import/order */
|
/* eslint-enable import/order */
|
||||||
|
|
||||||
|
// Setup global hook for improved Sentry state snapshots during initialization
|
||||||
|
const inTest = process.env.IN_TEST;
|
||||||
|
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
|
||||||
|
global.stateHooks.getMostRecentPersistedState = () =>
|
||||||
|
localStore.mostRecentRetrievedState;
|
||||||
|
|
||||||
const { sentry } = global;
|
const { sentry } = global;
|
||||||
const firstTimeState = { ...rawFirstTimeState };
|
const firstTimeState = { ...rawFirstTimeState };
|
||||||
|
|
||||||
@ -79,7 +88,7 @@ const metamaskInternalProcessHash = {
|
|||||||
|
|
||||||
const metamaskBlockedPorts = ['trezor-connect'];
|
const metamaskBlockedPorts = ['trezor-connect'];
|
||||||
|
|
||||||
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info');
|
log.setLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info', false);
|
||||||
|
|
||||||
const platform = new ExtensionPlatform();
|
const platform = new ExtensionPlatform();
|
||||||
const notificationManager = new NotificationManager();
|
const notificationManager = new NotificationManager();
|
||||||
@ -90,10 +99,6 @@ let uiIsTriggering = false;
|
|||||||
const openMetamaskTabsIDs = {};
|
const openMetamaskTabsIDs = {};
|
||||||
const requestAccountTabIds = {};
|
const requestAccountTabIds = {};
|
||||||
let controller;
|
let controller;
|
||||||
|
|
||||||
// state persistence
|
|
||||||
const inTest = process.env.IN_TEST;
|
|
||||||
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
|
|
||||||
let versionedData;
|
let versionedData;
|
||||||
|
|
||||||
if (inTest || process.env.METAMASK_DEBUG) {
|
if (inTest || process.env.METAMASK_DEBUG) {
|
||||||
@ -264,7 +269,8 @@ browser.runtime.onConnectExternal.addListener(async (...args) => {
|
|||||||
*/
|
*/
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
try {
|
try {
|
||||||
const initState = await loadStateFromPersistence();
|
const initData = await loadStateFromPersistence();
|
||||||
|
const initState = initData.data;
|
||||||
const initLangCode = await getFirstPreferredLangCode();
|
const initLangCode = await getFirstPreferredLangCode();
|
||||||
|
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
||||||
@ -287,6 +293,7 @@ async function initialize() {
|
|||||||
initLangCode,
|
initLangCode,
|
||||||
{},
|
{},
|
||||||
isFirstMetaMaskControllerSetup,
|
isFirstMetaMaskControllerSetup,
|
||||||
|
initData.meta,
|
||||||
);
|
);
|
||||||
if (!isManifestV3) {
|
if (!isManifestV3) {
|
||||||
await loadPhishingWarningPage();
|
await loadPhishingWarningPage();
|
||||||
@ -409,6 +416,19 @@ export async function loadStateFromPersistence() {
|
|||||||
versionedData = await migrator.migrateData(versionedData);
|
versionedData = await migrator.migrateData(versionedData);
|
||||||
if (!versionedData) {
|
if (!versionedData) {
|
||||||
throw new Error('MetaMask - migrator returned undefined');
|
throw new Error('MetaMask - migrator returned undefined');
|
||||||
|
} else if (!isObject(versionedData.meta)) {
|
||||||
|
throw new Error(
|
||||||
|
`MetaMask - migrator metadata has invalid type '${typeof versionedData.meta}'`,
|
||||||
|
);
|
||||||
|
} else if (typeof versionedData.meta.version !== 'number') {
|
||||||
|
throw new Error(
|
||||||
|
`MetaMask - migrator metadata version has invalid type '${typeof versionedData
|
||||||
|
.meta.version}'`,
|
||||||
|
);
|
||||||
|
} else if (!isObject(versionedData.data)) {
|
||||||
|
throw new Error(
|
||||||
|
`MetaMask - migrator data has invalid type '${typeof versionedData.data}'`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// this initializes the meta/version data as a class variable to be used for future writes
|
// this initializes the meta/version data as a class variable to be used for future writes
|
||||||
localStore.setMetadata(versionedData.meta);
|
localStore.setMetadata(versionedData.meta);
|
||||||
@ -417,7 +437,7 @@ export async function loadStateFromPersistence() {
|
|||||||
localStore.set(versionedData.data);
|
localStore.set(versionedData.data);
|
||||||
|
|
||||||
// return just the data
|
// return just the data
|
||||||
return versionedData.data;
|
return versionedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -430,12 +450,14 @@ export async function loadStateFromPersistence() {
|
|||||||
* @param {string} initLangCode - The region code for the language preferred by the current user.
|
* @param {string} initLangCode - The region code for the language preferred by the current user.
|
||||||
* @param {object} overrides - object with callbacks that are allowed to override the setup controller logic (usefull for desktop app)
|
* @param {object} overrides - object with callbacks that are allowed to override the setup controller logic (usefull for desktop app)
|
||||||
* @param isFirstMetaMaskControllerSetup
|
* @param isFirstMetaMaskControllerSetup
|
||||||
|
* @param {object} stateMetadata - Metadata about the initial state and migrations, including the most recent migration version
|
||||||
*/
|
*/
|
||||||
export function setupController(
|
export function setupController(
|
||||||
initState,
|
initState,
|
||||||
initLangCode,
|
initLangCode,
|
||||||
overrides,
|
overrides,
|
||||||
isFirstMetaMaskControllerSetup,
|
isFirstMetaMaskControllerSetup,
|
||||||
|
stateMetadata,
|
||||||
) {
|
) {
|
||||||
//
|
//
|
||||||
// MetaMask Controller
|
// MetaMask Controller
|
||||||
@ -462,6 +484,7 @@ export function setupController(
|
|||||||
localStore,
|
localStore,
|
||||||
overrides,
|
overrides,
|
||||||
isFirstMetaMaskControllerSetup,
|
isFirstMetaMaskControllerSetup,
|
||||||
|
currentMigrationVersion: stateMetadata.version,
|
||||||
});
|
});
|
||||||
|
|
||||||
setupEnsIpfsResolver({
|
setupEnsIpfsResolver({
|
||||||
@ -870,14 +893,9 @@ browser.runtime.onInstalled.addListener(({ reason }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function setupSentryGetStateGlobal(store) {
|
function setupSentryGetStateGlobal(store) {
|
||||||
global.stateHooks.getSentryState = function () {
|
global.stateHooks.getSentryAppState = function () {
|
||||||
const fullState = store.getState();
|
const backgroundState = store.memStore.getState();
|
||||||
const debugState = maskObject({ metamask: fullState }, SENTRY_STATE);
|
return maskObject(backgroundState, SENTRY_BACKGROUND_STATE);
|
||||||
return {
|
|
||||||
browser: window.navigator.userAgent,
|
|
||||||
store: debugState,
|
|
||||||
version: platform.getVersion(),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
104
app/scripts/controllers/app-metadata.test.ts
Normal file
104
app/scripts/controllers/app-metadata.test.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import AppMetadataController from './app-metadata';
|
||||||
|
|
||||||
|
const EXPECTED_DEFAULT_STATE = {
|
||||||
|
currentAppVersion: '',
|
||||||
|
previousAppVersion: '',
|
||||||
|
previousMigrationVersion: 0,
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('AppMetadataController', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('accepts initial state and does not modify it if currentMigrationVersion and platform.getVersion() match respective values in state', async () => {
|
||||||
|
const initState = {
|
||||||
|
currentAppVersion: '1',
|
||||||
|
previousAppVersion: '1',
|
||||||
|
previousMigrationVersion: 1,
|
||||||
|
currentMigrationVersion: 1,
|
||||||
|
};
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: initState,
|
||||||
|
currentMigrationVersion: 1,
|
||||||
|
currentAppVersion: '1',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), initState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets default state and does not modify it', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {},
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
appMetadataController.store.getState(),
|
||||||
|
EXPECTED_DEFAULT_STATE,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets default state and does not modify it if options version parameters match respective default values', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {},
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
currentAppVersion: '',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
appMetadataController.store.getState(),
|
||||||
|
EXPECTED_DEFAULT_STATE,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the currentAppVersion state property if options.currentAppVersion does not match the default value', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {},
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
currentAppVersion: '1',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), {
|
||||||
|
...EXPECTED_DEFAULT_STATE,
|
||||||
|
currentAppVersion: '1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the currentAppVersion and previousAppVersion state properties if options.currentAppVersion, currentAppVersion and previousAppVersion are all different', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {
|
||||||
|
currentAppVersion: '2',
|
||||||
|
previousAppVersion: '1',
|
||||||
|
},
|
||||||
|
currentAppVersion: '3',
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), {
|
||||||
|
...EXPECTED_DEFAULT_STATE,
|
||||||
|
currentAppVersion: '3',
|
||||||
|
previousAppVersion: '2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the currentMigrationVersion state property if the currentMigrationVersion param does not match the default value', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {},
|
||||||
|
currentMigrationVersion: 1,
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), {
|
||||||
|
...EXPECTED_DEFAULT_STATE,
|
||||||
|
currentMigrationVersion: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the currentMigrationVersion and previousMigrationVersion state properties if the currentMigrationVersion param, the currentMigrationVersion state property and the previousMigrationVersion state property are all different', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {
|
||||||
|
currentMigrationVersion: 2,
|
||||||
|
previousMigrationVersion: 1,
|
||||||
|
},
|
||||||
|
currentMigrationVersion: 3,
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), {
|
||||||
|
...EXPECTED_DEFAULT_STATE,
|
||||||
|
currentMigrationVersion: 3,
|
||||||
|
previousMigrationVersion: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
99
app/scripts/controllers/app-metadata.ts
Normal file
99
app/scripts/controllers/app-metadata.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import EventEmitter from 'events';
|
||||||
|
import { ObservableStore } from '@metamask/obs-store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of the AppMetadataController
|
||||||
|
*/
|
||||||
|
export type AppMetadataControllerState = {
|
||||||
|
currentAppVersion: string;
|
||||||
|
previousAppVersion: string;
|
||||||
|
previousMigrationVersion: number;
|
||||||
|
currentMigrationVersion: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options that NetworkController takes.
|
||||||
|
*/
|
||||||
|
export type AppMetadataControllerOptions = {
|
||||||
|
currentMigrationVersion?: number;
|
||||||
|
currentAppVersion?: string;
|
||||||
|
state?: Partial<AppMetadataControllerState>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultState: AppMetadataControllerState = {
|
||||||
|
currentAppVersion: '',
|
||||||
|
previousAppVersion: '',
|
||||||
|
previousMigrationVersion: 0,
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AppMetadata controller stores metadata about the current extension instance,
|
||||||
|
* including the currently and previously installed versions, and the most recently
|
||||||
|
* run migration.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class AppMetadataController extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* Observable store containing controller data.
|
||||||
|
*/
|
||||||
|
store: ObservableStore<AppMetadataControllerState>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a AppMetadata controller.
|
||||||
|
*
|
||||||
|
* @param options - the controller options
|
||||||
|
* @param options.state - Initial controller state.
|
||||||
|
* @param options.currentMigrationVersion
|
||||||
|
* @param options.currentAppVersion
|
||||||
|
*/
|
||||||
|
constructor({
|
||||||
|
currentAppVersion = '',
|
||||||
|
currentMigrationVersion = 0,
|
||||||
|
state = {},
|
||||||
|
}: AppMetadataControllerOptions) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.store = new ObservableStore({
|
||||||
|
...defaultState,
|
||||||
|
...state,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#maybeUpdateAppVersion(currentAppVersion);
|
||||||
|
|
||||||
|
this.#maybeUpdateMigrationVersion(currentMigrationVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the currentAppVersion in state, and sets the previousAppVersion to the old currentAppVersion.
|
||||||
|
*
|
||||||
|
* @param maybeNewAppVersion
|
||||||
|
*/
|
||||||
|
#maybeUpdateAppVersion(maybeNewAppVersion: string): void {
|
||||||
|
const oldCurrentAppVersion = this.store.getState().currentAppVersion;
|
||||||
|
|
||||||
|
if (maybeNewAppVersion !== oldCurrentAppVersion) {
|
||||||
|
this.store.updateState({
|
||||||
|
currentAppVersion: maybeNewAppVersion,
|
||||||
|
previousAppVersion: oldCurrentAppVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the migrationVersion in state.
|
||||||
|
*
|
||||||
|
* @param maybeNewMigrationVersion
|
||||||
|
*/
|
||||||
|
#maybeUpdateMigrationVersion(maybeNewMigrationVersion: number): void {
|
||||||
|
const oldCurrentMigrationVersion =
|
||||||
|
this.store.getState().currentMigrationVersion;
|
||||||
|
|
||||||
|
if (maybeNewMigrationVersion !== oldCurrentMigrationVersion) {
|
||||||
|
this.store.updateState({
|
||||||
|
previousMigrationVersion: oldCurrentMigrationVersion,
|
||||||
|
currentMigrationVersion: maybeNewMigrationVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,7 @@ export default class ComposableObservableStore extends ObservableStore {
|
|||||||
updateStructure(config) {
|
updateStructure(config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
|
const initialState = {};
|
||||||
for (const key of Object.keys(config)) {
|
for (const key of Object.keys(config)) {
|
||||||
if (!config[key]) {
|
if (!config[key]) {
|
||||||
throw new Error(`Undefined '${key}'`);
|
throw new Error(`Undefined '${key}'`);
|
||||||
@ -72,7 +73,10 @@ export default class ComposableObservableStore extends ObservableStore {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialState[key] = store.state ?? store.getState?.();
|
||||||
}
|
}
|
||||||
|
this.updateState(initialState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,6 +120,46 @@ describe('ComposableObservableStore', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should initialize state with all three types of stores', () => {
|
||||||
|
const controllerMessenger = new ControllerMessenger();
|
||||||
|
const exampleStore = new ObservableStore();
|
||||||
|
const exampleController = new ExampleController({
|
||||||
|
messenger: controllerMessenger,
|
||||||
|
});
|
||||||
|
const oldExampleController = new OldExampleController();
|
||||||
|
exampleStore.putState('state');
|
||||||
|
exampleController.updateBar('state');
|
||||||
|
oldExampleController.updateBaz('state');
|
||||||
|
const store = new ComposableObservableStore({ controllerMessenger });
|
||||||
|
|
||||||
|
store.updateStructure({
|
||||||
|
Example: exampleController,
|
||||||
|
OldExample: oldExampleController,
|
||||||
|
Store: exampleStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.getState()).toStrictEqual({
|
||||||
|
Example: { bar: 'state' },
|
||||||
|
OldExample: { baz: 'state' },
|
||||||
|
Store: 'state',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize falsy state', () => {
|
||||||
|
const controllerMessenger = new ControllerMessenger();
|
||||||
|
const exampleStore = new ObservableStore();
|
||||||
|
exampleStore.putState(false);
|
||||||
|
const store = new ComposableObservableStore({ controllerMessenger });
|
||||||
|
|
||||||
|
store.updateStructure({
|
||||||
|
Example: exampleStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.getState()).toStrictEqual({
|
||||||
|
Example: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should return flattened state', () => {
|
it('should return flattened state', () => {
|
||||||
const controllerMessenger = new ControllerMessenger();
|
const controllerMessenger = new ControllerMessenger();
|
||||||
const fooStore = new ObservableStore({ foo: 'foo' });
|
const fooStore = new ObservableStore({ foo: 'foo' });
|
||||||
|
@ -16,6 +16,7 @@ export default class ExtensionStore {
|
|||||||
// once data persistence fails once and it flips true we don't send further
|
// once data persistence fails once and it flips true we don't send further
|
||||||
// data persistence errors to sentry
|
// data persistence errors to sentry
|
||||||
this.dataPersistenceFailing = false;
|
this.dataPersistenceFailing = false;
|
||||||
|
this.mostRecentRetrievedState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMetadata(initMetaData) {
|
setMetadata(initMetaData) {
|
||||||
@ -66,8 +67,10 @@ export default class ExtensionStore {
|
|||||||
// extension.storage.local always returns an obj
|
// extension.storage.local always returns an obj
|
||||||
// if the object is empty, treat it as undefined
|
// if the object is empty, treat it as undefined
|
||||||
if (isEmpty(result)) {
|
if (isEmpty(result)) {
|
||||||
|
this.mostRecentRetrievedState = null;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
this.mostRecentRetrievedState = result;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,12 @@ import browser from 'webextension-polyfill';
|
|||||||
import LocalStore from './local-store';
|
import LocalStore from './local-store';
|
||||||
|
|
||||||
jest.mock('webextension-polyfill', () => ({
|
jest.mock('webextension-polyfill', () => ({
|
||||||
|
runtime: { lastError: null },
|
||||||
storage: { local: true },
|
storage: { local: true },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const setup = ({ isSupported }) => {
|
const setup = ({ localMock = jest.fn() } = {}) => {
|
||||||
browser.storage.local = isSupported;
|
browser.storage.local = localMock;
|
||||||
return new LocalStore();
|
return new LocalStore();
|
||||||
};
|
};
|
||||||
describe('LocalStore', () => {
|
describe('LocalStore', () => {
|
||||||
@ -15,21 +16,27 @@ describe('LocalStore', () => {
|
|||||||
});
|
});
|
||||||
describe('contructor', () => {
|
describe('contructor', () => {
|
||||||
it('should set isSupported property to false when browser does not support local storage', () => {
|
it('should set isSupported property to false when browser does not support local storage', () => {
|
||||||
const localStore = setup({ isSupported: false });
|
const localStore = setup({ localMock: false });
|
||||||
|
|
||||||
expect(localStore.isSupported).toBe(false);
|
expect(localStore.isSupported).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set isSupported property to true when browser supports local storage', () => {
|
it('should set isSupported property to true when browser supports local storage', () => {
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
expect(localStore.isSupported).toBe(true);
|
expect(localStore.isSupported).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should initialize mostRecentRetrievedState to null', () => {
|
||||||
|
const localStore = setup({ localMock: false });
|
||||||
|
|
||||||
|
expect(localStore.mostRecentRetrievedState).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setMetadata', () => {
|
describe('setMetadata', () => {
|
||||||
it('should set the metadata property on LocalStore', () => {
|
it('should set the metadata property on LocalStore', () => {
|
||||||
const metadata = { version: 74 };
|
const metadata = { version: 74 };
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
localStore.setMetadata(metadata);
|
localStore.setMetadata(metadata);
|
||||||
|
|
||||||
expect(localStore.metadata).toStrictEqual(metadata);
|
expect(localStore.metadata).toStrictEqual(metadata);
|
||||||
@ -38,21 +45,21 @@ describe('LocalStore', () => {
|
|||||||
|
|
||||||
describe('set', () => {
|
describe('set', () => {
|
||||||
it('should throw an error if called in a browser that does not support local storage', async () => {
|
it('should throw an error if called in a browser that does not support local storage', async () => {
|
||||||
const localStore = setup({ isSupported: false });
|
const localStore = setup({ localMock: false });
|
||||||
await expect(() => localStore.set()).rejects.toThrow(
|
await expect(() => localStore.set()).rejects.toThrow(
|
||||||
'Metamask- cannot persist state to local store as this browser does not support this action',
|
'Metamask- cannot persist state to local store as this browser does not support this action',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if not passed a truthy value as an argument', async () => {
|
it('should throw an error if not passed a truthy value as an argument', async () => {
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
await expect(() => localStore.set()).rejects.toThrow(
|
await expect(() => localStore.set()).rejects.toThrow(
|
||||||
'MetaMask - updated state is missing',
|
'MetaMask - updated state is missing',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if passed a valid argument but metadata has not yet been set', async () => {
|
it('should throw an error if passed a valid argument but metadata has not yet been set', async () => {
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
await expect(() =>
|
await expect(() =>
|
||||||
localStore.set({ appState: { test: true } }),
|
localStore.set({ appState: { test: true } }),
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
@ -61,7 +68,7 @@ describe('LocalStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw if passed a valid argument and metadata has been set', async () => {
|
it('should not throw if passed a valid argument and metadata has been set', async () => {
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
localStore.setMetadata({ version: 74 });
|
localStore.setMetadata({ version: 74 });
|
||||||
await expect(async function () {
|
await expect(async function () {
|
||||||
localStore.set({ appState: { test: true } });
|
localStore.set({ appState: { test: true } });
|
||||||
@ -71,9 +78,39 @@ describe('LocalStore', () => {
|
|||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
it('should return undefined if called in a browser that does not support local storage', async () => {
|
it('should return undefined if called in a browser that does not support local storage', async () => {
|
||||||
const localStore = setup({ isSupported: false });
|
const localStore = setup({ localMock: false });
|
||||||
const result = await localStore.get();
|
const result = await localStore.get();
|
||||||
expect(result).toStrictEqual(undefined);
|
expect(result).toStrictEqual(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update mostRecentRetrievedState', async () => {
|
||||||
|
const localStore = setup({
|
||||||
|
localMock: {
|
||||||
|
get: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve({ appState: { test: true } }),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await localStore.get();
|
||||||
|
|
||||||
|
expect(localStore.mostRecentRetrievedState).toStrictEqual({
|
||||||
|
appState: { test: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset mostRecentRetrievedState to null if storage.local is empty', async () => {
|
||||||
|
const localStore = setup({
|
||||||
|
localMock: {
|
||||||
|
get: jest.fn().mockImplementation(() => Promise.resolve({})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await localStore.get();
|
||||||
|
|
||||||
|
expect(localStore.mostRecentRetrievedState).toStrictEqual(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} Migration
|
* @typedef {object} Migration
|
||||||
@ -36,6 +37,8 @@ export default class Migrator extends EventEmitter {
|
|||||||
// perform each migration
|
// perform each migration
|
||||||
for (const migration of pendingMigrations) {
|
for (const migration of pendingMigrations) {
|
||||||
try {
|
try {
|
||||||
|
log.info(`Running migration ${migration.version}...`);
|
||||||
|
|
||||||
// attempt migration and validate
|
// attempt migration and validate
|
||||||
const migratedData = await migration.migrate(versionedData);
|
const migratedData = await migration.migrate(versionedData);
|
||||||
if (!migratedData.data) {
|
if (!migratedData.data) {
|
||||||
@ -52,6 +55,8 @@ export default class Migrator extends EventEmitter {
|
|||||||
// accept the migration as good
|
// accept the migration as good
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
versionedData = migratedData;
|
versionedData = migratedData;
|
||||||
|
|
||||||
|
log.info(`Migration ${migration.version} complete`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// rewrite error message to add context without clobbering stack
|
// rewrite error message to add context without clobbering stack
|
||||||
const originalErrorMessage = err.message;
|
const originalErrorMessage = err.message;
|
||||||
|
@ -15,6 +15,7 @@ export default class ReadOnlyNetworkStore {
|
|||||||
this._initialized = false;
|
this._initialized = false;
|
||||||
this._initializing = this._init();
|
this._initializing = this._init();
|
||||||
this._state = undefined;
|
this._state = undefined;
|
||||||
|
this.mostRecentRetrievedState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,6 +48,11 @@ export default class ReadOnlyNetworkStore {
|
|||||||
if (!this._initialized) {
|
if (!this._initialized) {
|
||||||
await this._initializing;
|
await this._initializing;
|
||||||
}
|
}
|
||||||
|
// Delay setting this until after the first read, to match the
|
||||||
|
// behavior of the local store.
|
||||||
|
if (!this.mostRecentRetrievedState) {
|
||||||
|
this.mostRecentRetrievedState = this._state;
|
||||||
|
}
|
||||||
return this._state;
|
return this._state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
76
app/scripts/lib/setup-initial-state-hooks.js
Normal file
76
app/scripts/lib/setup-initial-state-hooks.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { maskObject } from '../../../shared/modules/object.utils';
|
||||||
|
import ExtensionPlatform from '../platforms/extension';
|
||||||
|
import LocalStore from './local-store';
|
||||||
|
import ReadOnlyNetworkStore from './network-store';
|
||||||
|
import { SENTRY_BACKGROUND_STATE } from './setupSentry';
|
||||||
|
|
||||||
|
const platform = new ExtensionPlatform();
|
||||||
|
|
||||||
|
// This instance of `localStore` is used by Sentry to get the persisted state
|
||||||
|
const sentryLocalStore = process.env.IN_TEST
|
||||||
|
? new ReadOnlyNetworkStore()
|
||||||
|
: new LocalStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the persisted wallet state.
|
||||||
|
*
|
||||||
|
* @returns The persisted wallet state.
|
||||||
|
*/
|
||||||
|
globalThis.stateHooks.getPersistedState = async function () {
|
||||||
|
return await sentryLocalStore.get();
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistedStateMask = {
|
||||||
|
data: SENTRY_BACKGROUND_STATE,
|
||||||
|
meta: {
|
||||||
|
version: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a state snapshot for Sentry. This is used to add additional context to
|
||||||
|
* error reports, and it's used when processing errors and breadcrumbs to
|
||||||
|
* determine whether the user has opted into Metametrics.
|
||||||
|
*
|
||||||
|
* This uses the persisted state pre-initialization, and the in-memory state
|
||||||
|
* post-initialization. In both cases the state is anonymized.
|
||||||
|
*
|
||||||
|
* @returns A Sentry state snapshot.
|
||||||
|
*/
|
||||||
|
globalThis.stateHooks.getSentryState = function () {
|
||||||
|
const sentryState = {
|
||||||
|
browser: window.navigator.userAgent,
|
||||||
|
version: platform.getVersion(),
|
||||||
|
};
|
||||||
|
// If `getSentryAppState` is set, it implies that initialization has completed
|
||||||
|
if (globalThis.stateHooks.getSentryAppState) {
|
||||||
|
return {
|
||||||
|
...sentryState,
|
||||||
|
state: globalThis.stateHooks.getSentryAppState(),
|
||||||
|
};
|
||||||
|
} else if (
|
||||||
|
// This is truthy if Sentry has retrieved state at least once already. This
|
||||||
|
// should always be true when getting context for an error report, but can
|
||||||
|
// be unset when Sentry is performing the opt-in check.
|
||||||
|
sentryLocalStore.mostRecentRetrievedState ||
|
||||||
|
// This is only set in the background process.
|
||||||
|
globalThis.stateHooks.getMostRecentPersistedState
|
||||||
|
) {
|
||||||
|
const persistedState =
|
||||||
|
sentryLocalStore.mostRecentRetrievedState ||
|
||||||
|
globalThis.stateHooks.getMostRecentPersistedState();
|
||||||
|
// This can be unset when this method is called in the background for an
|
||||||
|
// opt-in check, but the state hasn't been loaded yet.
|
||||||
|
if (persistedState) {
|
||||||
|
return {
|
||||||
|
...sentryState,
|
||||||
|
persistedState: maskObject(persistedState, persistedStateMask),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This branch means that local storage has not yet been read, so we have
|
||||||
|
// no choice but to omit the application state.
|
||||||
|
// This should be unreachable when getting context for an error report, but
|
||||||
|
// can be false when Sentry is performing the opt-in check.
|
||||||
|
return sentryState;
|
||||||
|
};
|
@ -1,10 +0,0 @@
|
|||||||
import LocalStore from './local-store';
|
|
||||||
import ReadOnlyNetworkStore from './network-store';
|
|
||||||
|
|
||||||
const localStore = process.env.IN_TEST
|
|
||||||
? new ReadOnlyNetworkStore()
|
|
||||||
: new LocalStore();
|
|
||||||
|
|
||||||
globalThis.stateHooks.getPersistedState = async function () {
|
|
||||||
return await localStore.get();
|
|
||||||
};
|
|
@ -23,58 +23,175 @@ export const ERROR_URL_ALLOWLIST = {
|
|||||||
SEGMENT: 'segment.io',
|
SEGMENT: 'segment.io',
|
||||||
};
|
};
|
||||||
|
|
||||||
// This describes the subset of Redux state attached to errors sent to Sentry
|
// This describes the subset of background controller state attached to errors
|
||||||
// These properties have some potential to be useful for debugging, and they do
|
// sent to Sentry These properties have some potential to be useful for
|
||||||
// not contain any identifiable information.
|
// debugging, and they do not contain any identifiable information.
|
||||||
export const SENTRY_STATE = {
|
export const SENTRY_BACKGROUND_STATE = {
|
||||||
gas: true,
|
AccountTracker: {
|
||||||
history: true,
|
currentBlockGasLimit: true,
|
||||||
metamask: {
|
},
|
||||||
|
AlertController: {
|
||||||
alertEnabledness: true,
|
alertEnabledness: true,
|
||||||
completedOnboarding: true,
|
},
|
||||||
|
AppMetadataController: {
|
||||||
|
currentAppVersion: true,
|
||||||
|
previousAppVersion: true,
|
||||||
|
previousMigrationVersion: true,
|
||||||
|
currentMigrationVersion: true,
|
||||||
|
},
|
||||||
|
AppStateController: {
|
||||||
connectedStatusPopoverHasBeenShown: true,
|
connectedStatusPopoverHasBeenShown: true,
|
||||||
|
defaultHomeActiveTabName: true,
|
||||||
|
},
|
||||||
|
CurrencyController: {
|
||||||
conversionDate: true,
|
conversionDate: true,
|
||||||
conversionRate: true,
|
conversionRate: true,
|
||||||
currentBlockGasLimit: true,
|
|
||||||
currentCurrency: true,
|
currentCurrency: true,
|
||||||
currentLocale: true,
|
|
||||||
customNonceValue: true,
|
|
||||||
defaultHomeActiveTabName: true,
|
|
||||||
desktopEnabled: true,
|
|
||||||
featureFlags: true,
|
|
||||||
firstTimeFlowType: true,
|
|
||||||
forgottenPassword: true,
|
|
||||||
incomingTxLastFetchedBlockByChainId: true,
|
|
||||||
ipfsGateway: true,
|
|
||||||
isAccountMenuOpen: true,
|
|
||||||
isInitialized: true,
|
|
||||||
isUnlocked: true,
|
|
||||||
metaMetricsId: true,
|
|
||||||
nativeCurrency: true,
|
nativeCurrency: true,
|
||||||
|
},
|
||||||
|
DecryptMessageController: {
|
||||||
|
unapprovedDecryptMsgCount: true,
|
||||||
|
},
|
||||||
|
DesktopController: {
|
||||||
|
desktopEnabled: true,
|
||||||
|
},
|
||||||
|
EncryptionPublicKeyController: {
|
||||||
|
unapprovedEncryptionPublicKeyMsgCount: true,
|
||||||
|
},
|
||||||
|
IncomingTransactionsController: {
|
||||||
|
incomingTxLastFetchedBlockByChainId: true,
|
||||||
|
},
|
||||||
|
KeyringController: {
|
||||||
|
isUnlocked: true,
|
||||||
|
},
|
||||||
|
MetaMetricsController: {
|
||||||
|
metaMetricsId: true,
|
||||||
|
participateInMetaMetrics: true,
|
||||||
|
},
|
||||||
|
NetworkController: {
|
||||||
networkId: true,
|
networkId: true,
|
||||||
networkStatus: true,
|
networkStatus: true,
|
||||||
nextNonce: true,
|
|
||||||
participateInMetaMetrics: true,
|
|
||||||
preferences: true,
|
|
||||||
providerConfig: {
|
providerConfig: {
|
||||||
nickname: true,
|
nickname: true,
|
||||||
ticker: true,
|
ticker: true,
|
||||||
type: true,
|
type: true,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
OnboardingController: {
|
||||||
|
completedOnboarding: true,
|
||||||
|
firstTimeFlowType: true,
|
||||||
seedPhraseBackedUp: true,
|
seedPhraseBackedUp: true,
|
||||||
unapprovedDecryptMsgCount: true,
|
},
|
||||||
unapprovedEncryptionPublicKeyMsgCount: true,
|
PreferencesController: {
|
||||||
unapprovedMsgCount: true,
|
currentLocale: true,
|
||||||
unapprovedPersonalMsgCount: true,
|
featureFlags: true,
|
||||||
unapprovedTypedMessagesCount: true,
|
forgottenPassword: true,
|
||||||
|
ipfsGateway: true,
|
||||||
|
preferences: true,
|
||||||
useBlockie: true,
|
useBlockie: true,
|
||||||
useNonceField: true,
|
useNonceField: true,
|
||||||
usePhishDetect: true,
|
usePhishDetect: true,
|
||||||
|
},
|
||||||
|
SignatureController: {
|
||||||
|
unapprovedMsgCount: true,
|
||||||
|
unapprovedPersonalMsgCount: true,
|
||||||
|
unapprovedTypedMessagesCount: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenedBackgroundStateMask = Object.values(
|
||||||
|
SENTRY_BACKGROUND_STATE,
|
||||||
|
).reduce((partialBackgroundState, controllerState) => {
|
||||||
|
return {
|
||||||
|
...partialBackgroundState,
|
||||||
|
...controllerState,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// This describes the subset of Redux state attached to errors sent to Sentry
|
||||||
|
// These properties have some potential to be useful for debugging, and they do
|
||||||
|
// not contain any identifiable information.
|
||||||
|
export const SENTRY_UI_STATE = {
|
||||||
|
gas: true,
|
||||||
|
history: true,
|
||||||
|
metamask: {
|
||||||
|
...flattenedBackgroundStateMask,
|
||||||
|
// This property comes from the background but isn't in controller state
|
||||||
|
isInitialized: true,
|
||||||
|
// These properties are in the `metamask` slice but not in the background state
|
||||||
|
customNonceValue: true,
|
||||||
|
isAccountMenuOpen: true,
|
||||||
|
nextNonce: true,
|
||||||
welcomeScreenSeen: true,
|
welcomeScreenSeen: true,
|
||||||
},
|
},
|
||||||
unconnectedAccount: true,
|
unconnectedAccount: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether MetaMetrics is enabled, given the application state.
|
||||||
|
*
|
||||||
|
* @param {{ state: unknown} | { persistedState: unknown }} appState - Application state
|
||||||
|
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
||||||
|
* is enabled, `false` otherwise.
|
||||||
|
*/
|
||||||
|
function getMetaMetricsEnabledFromAppState(appState) {
|
||||||
|
// during initialization after loading persisted state
|
||||||
|
if (appState.persistedState) {
|
||||||
|
return getMetaMetricsEnabledFromPersistedState(appState.persistedState);
|
||||||
|
// After initialization
|
||||||
|
} else if (appState.state) {
|
||||||
|
// UI
|
||||||
|
if (appState.state.metamask) {
|
||||||
|
return Boolean(appState.state.metamask.participateInMetaMetrics);
|
||||||
|
}
|
||||||
|
// background
|
||||||
|
return Boolean(
|
||||||
|
appState.state.MetaMetricsController?.participateInMetaMetrics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// during initialization, before first persisted state is read
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether MetaMetrics is enabled, given the persisted state.
|
||||||
|
*
|
||||||
|
* @param {unknown} persistedState - Application state
|
||||||
|
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
||||||
|
* is enabled, `false` otherwise.
|
||||||
|
*/
|
||||||
|
function getMetaMetricsEnabledFromPersistedState(persistedState) {
|
||||||
|
return Boolean(
|
||||||
|
persistedState?.data?.MetaMetricsController?.participateInMetaMetrics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether onboarding has completed, given the application state.
|
||||||
|
*
|
||||||
|
* @param {Record<string, unknown>} appState - Application state
|
||||||
|
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
||||||
|
* is enabled, `false` otherwise.
|
||||||
|
*/
|
||||||
|
function getOnboardingCompleteFromAppState(appState) {
|
||||||
|
// during initialization after loading persisted state
|
||||||
|
if (appState.persistedState) {
|
||||||
|
return Boolean(
|
||||||
|
appState.persistedState.data?.OnboardingController?.completedOnboarding,
|
||||||
|
);
|
||||||
|
// After initialization
|
||||||
|
} else if (appState.state) {
|
||||||
|
// UI
|
||||||
|
if (appState.state.metamask) {
|
||||||
|
return Boolean(appState.state.metamask.completedOnboarding);
|
||||||
|
}
|
||||||
|
// background
|
||||||
|
return Boolean(appState.state.OnboardingController?.completedOnboarding);
|
||||||
|
}
|
||||||
|
// during initialization, before first persisted state is read
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export default function setupSentry({ release, getState }) {
|
export default function setupSentry({ release, getState }) {
|
||||||
if (!release) {
|
if (!release) {
|
||||||
throw new Error('Missing release');
|
throw new Error('Missing release');
|
||||||
@ -112,22 +229,21 @@ export default function setupSentry({ release, getState }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that returns whether MetaMetrics is enabled. This should also
|
* Returns whether MetaMetrics is enabled. If the application hasn't yet
|
||||||
* return `false` if state has not yet been initialzed.
|
* been initialized, the persisted state will be used (if any).
|
||||||
*
|
*
|
||||||
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
* @returns `true` if MetaMetrics is enabled, `false` otherwise.
|
||||||
* is enabled, `false` otherwise.
|
|
||||||
*/
|
*/
|
||||||
async function getMetaMetricsEnabled() {
|
async function getMetaMetricsEnabled() {
|
||||||
const appState = getState();
|
const appState = getState();
|
||||||
if (Object.keys(appState) > 0) {
|
if (appState.state || appState.persistedState) {
|
||||||
return Boolean(appState?.store?.metamask?.participateInMetaMetrics);
|
return getMetaMetricsEnabledFromAppState(appState);
|
||||||
}
|
}
|
||||||
|
// If we reach here, it means the error was thrown before initialization
|
||||||
|
// completed, and before we loaded the persisted state for the first time.
|
||||||
try {
|
try {
|
||||||
const persistedState = await globalThis.stateHooks.getPersistedState();
|
const persistedState = await globalThis.stateHooks.getPersistedState();
|
||||||
return Boolean(
|
return getMetaMetricsEnabledFromPersistedState(persistedState);
|
||||||
persistedState?.data?.MetaMetricsController?.participateInMetaMetrics,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return false;
|
return false;
|
||||||
@ -175,17 +291,15 @@ function hideUrlIfNotInternal(url) {
|
|||||||
*/
|
*/
|
||||||
export function beforeBreadcrumb(getState) {
|
export function beforeBreadcrumb(getState) {
|
||||||
return (breadcrumb) => {
|
return (breadcrumb) => {
|
||||||
if (getState) {
|
if (!getState) {
|
||||||
const appState = getState();
|
return null;
|
||||||
if (
|
}
|
||||||
Object.values(appState).length &&
|
const appState = getState();
|
||||||
(!appState?.store?.metamask?.participateInMetaMetrics ||
|
if (
|
||||||
!appState?.store?.metamask?.completedOnboarding ||
|
!getMetaMetricsEnabledFromAppState(appState) ||
|
||||||
breadcrumb?.category === 'ui.input')
|
!getOnboardingCompleteFromAppState(appState) ||
|
||||||
) {
|
breadcrumb?.category === 'ui.input'
|
||||||
return null;
|
) {
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb);
|
const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb);
|
||||||
|
@ -28,7 +28,7 @@ try {
|
|||||||
const namedIntrinsics = Reflect.ownKeys(new Compartment().globalThis);
|
const namedIntrinsics = Reflect.ownKeys(new Compartment().globalThis);
|
||||||
|
|
||||||
// These named intrinsics are not automatically hardened by `lockdown`
|
// These named intrinsics are not automatically hardened by `lockdown`
|
||||||
const shouldHardenManually = new Set(['eval', 'Function']);
|
const shouldHardenManually = new Set(['eval', 'Function', 'Symbol']);
|
||||||
|
|
||||||
const globalProperties = new Set([
|
const globalProperties = new Set([
|
||||||
// universalPropertyNames is a constant added by lockdown to global scope
|
// universalPropertyNames is a constant added by lockdown to global scope
|
||||||
|
@ -195,6 +195,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 EncryptionPublicKeyController from './controllers/encryption-public-key';
|
import EncryptionPublicKeyController from './controllers/encryption-public-key';
|
||||||
|
import AppMetadataController from './controllers/app-metadata';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CaveatMutatorFactories,
|
CaveatMutatorFactories,
|
||||||
@ -264,6 +265,8 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
// instance of a class that wraps the extension's storage local API.
|
// instance of a class that wraps the extension's storage local API.
|
||||||
this.localStoreApiWrapper = opts.localStore;
|
this.localStoreApiWrapper = opts.localStore;
|
||||||
|
|
||||||
|
this.currentMigrationVersion = opts.currentMigrationVersion;
|
||||||
|
|
||||||
// observable state store
|
// observable state store
|
||||||
this.store = new ComposableObservableStore({
|
this.store = new ComposableObservableStore({
|
||||||
state: initState,
|
state: initState,
|
||||||
@ -284,6 +287,12 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.appMetadataController = new AppMetadataController({
|
||||||
|
state: initState.AppMetadataController,
|
||||||
|
currentMigrationVersion: this.currentMigrationVersion,
|
||||||
|
currentAppVersion: version,
|
||||||
|
});
|
||||||
|
|
||||||
// next, we will initialize the controllers
|
// next, we will initialize the controllers
|
||||||
// controller initialization order matters
|
// controller initialization order matters
|
||||||
|
|
||||||
@ -1558,6 +1567,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
|
|
||||||
this.store.updateStructure({
|
this.store.updateStructure({
|
||||||
AppStateController: this.appStateController.store,
|
AppStateController: this.appStateController.store,
|
||||||
|
AppMetadataController: this.appMetadataController.store,
|
||||||
TransactionController: this.txController.store,
|
TransactionController: this.txController.store,
|
||||||
KeyringController: this.keyringController.store,
|
KeyringController: this.keyringController.store,
|
||||||
PreferencesController: this.preferencesController.store,
|
PreferencesController: this.preferencesController.store,
|
||||||
@ -1602,6 +1612,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
this.memStore = new ComposableObservableStore({
|
this.memStore = new ComposableObservableStore({
|
||||||
config: {
|
config: {
|
||||||
AppStateController: this.appStateController.store,
|
AppStateController: this.appStateController.store,
|
||||||
|
AppMetadataController: this.appMetadataController.store,
|
||||||
NetworkController: this.networkController,
|
NetworkController: this.networkController,
|
||||||
CachedBalancesController: this.cachedBalancesController.store,
|
CachedBalancesController: this.cachedBalancesController.store,
|
||||||
KeyringController: this.keyringController.memStore,
|
KeyringController: this.keyringController.memStore,
|
||||||
@ -2696,6 +2707,9 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
assetsContractController.getBalancesInSingleCall.bind(
|
assetsContractController.getBalancesInSingleCall.bind(
|
||||||
assetsContractController,
|
assetsContractController,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// E2E testing
|
||||||
|
throwTestError: this.throwTestError.bind(this),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4413,6 +4427,21 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
return nonceLock.nextNonce;
|
return nonceLock.nextNonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an artificial error in a timeout handler for testing purposes.
|
||||||
|
*
|
||||||
|
* @param message - The error message.
|
||||||
|
* @deprecated This is only mean to facilitiate E2E testing. We should not
|
||||||
|
* use this for handling errors.
|
||||||
|
*/
|
||||||
|
throwTestError(message) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const error = new Error(message);
|
||||||
|
error.name = 'TestError';
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// CONFIG
|
// CONFIG
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
|
import log from 'loglevel';
|
||||||
|
import { hasProperty, isObject } from '@metamask/utils';
|
||||||
import transformState077For082 from './077-supplements/077-supplement-for-082';
|
import transformState077For082 from './077-supplements/077-supplement-for-082';
|
||||||
import transformState077For084 from './077-supplements/077-supplement-for-084';
|
import transformState077For084 from './077-supplements/077-supplement-for-084';
|
||||||
import transformState077For086 from './077-supplements/077-supplement-for-086';
|
import transformState077For086 from './077-supplements/077-supplement-for-086';
|
||||||
@ -29,8 +31,23 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function transformState(state) {
|
function transformState(state) {
|
||||||
const TokenListController = state?.TokenListController || {};
|
if (!hasProperty(state, 'TokenListController')) {
|
||||||
|
log.warn('Skipping migration, TokenListController state is missing');
|
||||||
|
return state;
|
||||||
|
} else if (!isObject(state.TokenListController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.TokenListController is ${typeof state.TokenListController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
} else if (!hasProperty(state.TokenListController, 'tokensChainsCache')) {
|
||||||
|
log.warn(
|
||||||
|
'Skipping migration, TokenListController.tokensChainsCache state is missing',
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
const { TokenListController } = state;
|
||||||
const { tokensChainsCache } = TokenListController;
|
const { tokensChainsCache } = TokenListController;
|
||||||
|
|
||||||
let dataCache;
|
let dataCache;
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import migration77 from './077';
|
import migration77 from './077';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
startSession: jest.fn(),
|
||||||
|
endSession: jest.fn(),
|
||||||
|
toggleSession: jest.fn(),
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
describe('migration #77', () => {
|
describe('migration #77', () => {
|
||||||
it('should update the version metadata', async () => {
|
it('should update the version metadata', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
version: 76,
|
version: 76,
|
||||||
},
|
},
|
||||||
|
data: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const newStorage = await migration77.migrate(oldStorage);
|
const newStorage = await migration77.migrate(oldStorage);
|
||||||
@ -13,6 +24,65 @@ describe('migration #77', () => {
|
|||||||
version: 77,
|
version: 77,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return state unchanged if token list controller is missing', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 76,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
Foo: {
|
||||||
|
bar: 'baz',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStorage = await migration77.migrate(cloneDeep(oldStorage));
|
||||||
|
|
||||||
|
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if the TokenListController state is invalid', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 76,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
TokenListController: 'test',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await migration77.migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokenListController is string`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return state unchanged if tokenChainsCache is missing', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 76,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
TokenListController: {
|
||||||
|
tokenList: {
|
||||||
|
'0x514910771af9ca656af840dff83e8264ecf986ca': {
|
||||||
|
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
|
||||||
|
symbol: 'LINK',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStorage = await migration77.migrate(cloneDeep(oldStorage));
|
||||||
|
|
||||||
|
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
||||||
|
});
|
||||||
|
|
||||||
it('should change the data from array to object for a single network', async () => {
|
it('should change the data from array to object for a single network', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { migrate, version } from './082';
|
import { migrate, version } from './082';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock('uuid', () => {
|
jest.mock('uuid', () => {
|
||||||
const actual = jest.requireActual('uuid');
|
const actual = jest.requireActual('uuid');
|
||||||
|
|
||||||
@ -472,10 +478,72 @@ describe('migration #82', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const newStorage = await migrate(oldStorage);
|
const newStorage = await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalled();
|
||||||
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not change anything if there is no frequentRpcListDetail property on PreferencesController', async () => {
|
it('should capture an exception if any PreferencesController.frequentRpcListDetail entries are not objects', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 81,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
PreferencesController: {
|
||||||
|
transactionSecurityCheckEnabled: false,
|
||||||
|
useBlockie: false,
|
||||||
|
useCurrencyRateCheck: true,
|
||||||
|
useMultiAccountBalanceChecker: true,
|
||||||
|
useNftDetection: false,
|
||||||
|
useNonceField: false,
|
||||||
|
frequentRpcListDetail: [
|
||||||
|
{
|
||||||
|
chainId: '0x539',
|
||||||
|
nickname: 'Localhost 8545',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: 'http://localhost:8545',
|
||||||
|
ticker: 'ETH',
|
||||||
|
},
|
||||||
|
'invalid entry type',
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
NetworkController: {
|
||||||
|
network: '1',
|
||||||
|
networkDetails: {
|
||||||
|
EIPS: {
|
||||||
|
1559: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
previousProviderStore: {
|
||||||
|
chainId: '0x89',
|
||||||
|
nickname: 'Polygon Mainnet',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl:
|
||||||
|
'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
|
||||||
|
ticker: 'MATIC',
|
||||||
|
type: 'rpc',
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
chainId: '0x1',
|
||||||
|
nickname: '',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: '',
|
||||||
|
ticker: 'ETH',
|
||||||
|
type: 'mainnet',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(
|
||||||
|
`state.PreferencesController.frequentRpcListDetail contains an element of type string`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change anything, and not capture an exception, if there is no frequentRpcListDetail property on PreferencesController but there is a networkConfigurations object', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
version: 81,
|
version: 81,
|
||||||
@ -556,9 +624,60 @@ describe('migration #82', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const newStorage = await migrate(oldStorage);
|
const newStorage = await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).not.toHaveBeenCalled();
|
||||||
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if there is no frequentRpcListDetail property on PreferencesController and no networkConfiguration object', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 81,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
PreferencesController: {
|
||||||
|
transactionSecurityCheckEnabled: false,
|
||||||
|
useBlockie: false,
|
||||||
|
useCurrencyRateCheck: true,
|
||||||
|
useMultiAccountBalanceChecker: true,
|
||||||
|
useNftDetection: false,
|
||||||
|
useNonceField: false,
|
||||||
|
},
|
||||||
|
NetworkController: {
|
||||||
|
network: '1',
|
||||||
|
networkDetails: {
|
||||||
|
EIPS: {
|
||||||
|
1559: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
previousProviderStore: {
|
||||||
|
chainId: '0x89',
|
||||||
|
nickname: 'Polygon Mainnet',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl:
|
||||||
|
'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
|
||||||
|
ticker: 'MATIC',
|
||||||
|
type: 'rpc',
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
chainId: '0x1',
|
||||||
|
nickname: '',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: '',
|
||||||
|
ticker: 'ETH',
|
||||||
|
type: 'mainnet',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(
|
||||||
|
`typeof state.PreferencesController.frequentRpcListDetail is undefined`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should change nothing if PreferencesController is undefined', async () => {
|
it('should change nothing if PreferencesController is undefined', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -595,4 +714,61 @@ describe('migration #82', () => {
|
|||||||
const newStorage = await migrate(oldStorage);
|
const newStorage = await migrate(oldStorage);
|
||||||
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if PreferencesController is not an object', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 81,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
NetworkController: {
|
||||||
|
network: '1',
|
||||||
|
networkDetails: {
|
||||||
|
EIPS: {
|
||||||
|
1559: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
previousProviderStore: {
|
||||||
|
chainId: '0x89',
|
||||||
|
nickname: 'Polygon Mainnet',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl:
|
||||||
|
'https://polygon-mainnet.infura.io/v3/373266a93aab4acda48f89d4fe77c748',
|
||||||
|
ticker: 'MATIC',
|
||||||
|
type: 'rpc',
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
chainId: '0x1',
|
||||||
|
nickname: '',
|
||||||
|
rpcPrefs: {},
|
||||||
|
rpcUrl: '',
|
||||||
|
ticker: 'ETH',
|
||||||
|
type: 'mainnet',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreferencesController: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.PreferencesController is boolean`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if NetworkController is undefined', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 81,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
PreferencesController: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { hasProperty, isObject } from '@metamask/utils';
|
import { hasProperty, isObject } from '@metamask/utils';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
export const version = 82;
|
export const version = 82;
|
||||||
|
|
||||||
@ -25,14 +26,56 @@ export async function migrate(originalVersionedData: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformState(state: Record<string, unknown>) {
|
function transformState(state: Record<string, unknown>) {
|
||||||
|
if (!hasProperty(state, 'PreferencesController')) {
|
||||||
|
log.warn(`state.PreferencesController is undefined`);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!isObject(state.PreferencesController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.PreferencesController is ${typeof state.PreferencesController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!hasProperty(state, 'PreferencesController') ||
|
!hasProperty(state, 'NetworkController') ||
|
||||||
!isObject(state.PreferencesController) ||
|
!isObject(state.NetworkController)
|
||||||
!isObject(state.NetworkController) ||
|
|
||||||
!hasProperty(state.PreferencesController, 'frequentRpcListDetail') ||
|
|
||||||
!Array.isArray(state.PreferencesController.frequentRpcListDetail) ||
|
|
||||||
!state.PreferencesController.frequentRpcListDetail.every(isObject)
|
|
||||||
) {
|
) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController is ${typeof state.NetworkController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!hasProperty(state.PreferencesController, 'frequentRpcListDetail') ||
|
||||||
|
!Array.isArray(state.PreferencesController.frequentRpcListDetail)
|
||||||
|
) {
|
||||||
|
const inPost077SupplementFor082State =
|
||||||
|
state.NetworkController.networkConfigurations &&
|
||||||
|
state.PreferencesController.frequentRpcListDetail === undefined;
|
||||||
|
if (!inPost077SupplementFor082State) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.PreferencesController.frequentRpcListDetail is ${typeof state
|
||||||
|
.PreferencesController.frequentRpcListDetail}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!state.PreferencesController.frequentRpcListDetail.every(isObject)) {
|
||||||
|
const erroneousElement =
|
||||||
|
state.PreferencesController.frequentRpcListDetail.find(
|
||||||
|
(element) => !isObject(element),
|
||||||
|
);
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`state.PreferencesController.frequentRpcListDetail contains an element of type ${typeof erroneousElement}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
const { PreferencesController, NetworkController } = state;
|
const { PreferencesController, NetworkController } = state;
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { migrate, version } from './083';
|
import { migrate, version } from './083';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock('uuid', () => {
|
jest.mock('uuid', () => {
|
||||||
const actual = jest.requireActual('uuid');
|
const actual = jest.requireActual('uuid');
|
||||||
|
|
||||||
@ -165,6 +171,24 @@ describe('migration #83', () => {
|
|||||||
expect(newStorage).toStrictEqual(expectedNewStorage);
|
expect(newStorage).toStrictEqual(expectedNewStorage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if state.NetworkController is undefined', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
testProperty: 'testValue',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not modify state if state.NetworkController is not an object', async () => {
|
it('should not modify state if state.NetworkController is not an object', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -190,6 +214,25 @@ describe('migration #83', () => {
|
|||||||
expect(newStorage).toStrictEqual(expectedNewStorage);
|
expect(newStorage).toStrictEqual(expectedNewStorage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if state.NetworkController is not an object', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
NetworkController: false,
|
||||||
|
testProperty: 'testValue',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController is boolean`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not modify state if state.NetworkController.networkConfigurations is undefined', async () => {
|
it('should not modify state if state.NetworkController.networkConfigurations is undefined', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -221,6 +264,28 @@ describe('migration #83', () => {
|
|||||||
expect(newStorage).toStrictEqual(expectedNewStorage);
|
expect(newStorage).toStrictEqual(expectedNewStorage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if state.NetworkController.networkConfigurations is undefined', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
NetworkController: {
|
||||||
|
testNetworkControllerProperty: 'testNetworkControllerValue',
|
||||||
|
networkConfigurations: undefined,
|
||||||
|
},
|
||||||
|
testProperty: 'testValue',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof NetworkController.networkConfigurations is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not modify state if state.NetworkController.networkConfigurations is an empty object', async () => {
|
it('should not modify state if state.NetworkController.networkConfigurations is an empty object', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -25,11 +25,21 @@ export async function migrate(originalVersionedData: {
|
|||||||
|
|
||||||
function transformState(state: Record<string, unknown>) {
|
function transformState(state: Record<string, unknown>) {
|
||||||
if (!isObject(state.NetworkController)) {
|
if (!isObject(state.NetworkController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController is ${typeof state.NetworkController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
const { NetworkController } = state;
|
const { NetworkController } = state;
|
||||||
|
|
||||||
if (!isObject(NetworkController.networkConfigurations)) {
|
if (!isObject(NetworkController.networkConfigurations)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof NetworkController.networkConfigurations is ${typeof NetworkController.networkConfigurations}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import { migrate } from './084';
|
import { migrate } from './084';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
describe('migration #84', () => {
|
describe('migration #84', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('updates the version metadata', async () => {
|
it('updates the version metadata', async () => {
|
||||||
const originalVersionedData = buildOriginalVersionedData({
|
const originalVersionedData = buildOriginalVersionedData({
|
||||||
meta: {
|
meta: {
|
||||||
@ -27,6 +37,21 @@ describe('migration #84', () => {
|
|||||||
expect(newVersionedData.data).toStrictEqual(originalVersionedData.data);
|
expect(newVersionedData.data).toStrictEqual(originalVersionedData.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the network controller state does not exist', async () => {
|
||||||
|
const originalVersionedData = buildOriginalVersionedData({
|
||||||
|
data: {
|
||||||
|
test: '123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await migrate(originalVersionedData);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const nonObjects = [undefined, null, 'test', 1, ['test']];
|
const nonObjects = [undefined, null, 'test', 1, ['test']];
|
||||||
for (const invalidState of nonObjects) {
|
for (const invalidState of nonObjects) {
|
||||||
it(`does not change the state if the network controller state is ${invalidState}`, async () => {
|
it(`does not change the state if the network controller state is ${invalidState}`, async () => {
|
||||||
@ -40,6 +65,21 @@ describe('migration #84', () => {
|
|||||||
|
|
||||||
expect(newVersionedData.data).toStrictEqual(originalVersionedData.data);
|
expect(newVersionedData.data).toStrictEqual(originalVersionedData.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(`captures an exception if the network controller state is ${invalidState}`, async () => {
|
||||||
|
const originalVersionedData = buildOriginalVersionedData({
|
||||||
|
data: {
|
||||||
|
NetworkController: invalidState,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await migrate(originalVersionedData);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController is ${typeof invalidState}`),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it('does not change the state if the network controller state does not include "network"', async () => {
|
it('does not change the state if the network controller state does not include "network"', async () => {
|
||||||
@ -56,6 +96,38 @@ describe('migration #84', () => {
|
|||||||
expect(newVersionedData.data).toStrictEqual(originalVersionedData.data);
|
expect(newVersionedData.data).toStrictEqual(originalVersionedData.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the network controller state does not include "network" and does not include "networkId"', async () => {
|
||||||
|
const originalVersionedData = buildOriginalVersionedData({
|
||||||
|
data: {
|
||||||
|
NetworkController: {
|
||||||
|
test: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await migrate(originalVersionedData);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController.network is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not capture an exception if the network controller state does not include "network" but does include "networkId"', async () => {
|
||||||
|
const originalVersionedData = buildOriginalVersionedData({
|
||||||
|
data: {
|
||||||
|
NetworkController: {
|
||||||
|
test: '123',
|
||||||
|
networkId: 'foobar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await migrate(originalVersionedData);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('replaces "network" in the network controller state with "networkId": null, "networkStatus": "unknown" if it is "loading"', async () => {
|
it('replaces "network" in the network controller state with "networkId": null, "networkStatus": "unknown" if it is "loading"', async () => {
|
||||||
const originalVersionedData = buildOriginalVersionedData({
|
const originalVersionedData = buildOriginalVersionedData({
|
||||||
data: {
|
data: {
|
||||||
|
@ -25,9 +25,26 @@ export async function migrate(originalVersionedData: {
|
|||||||
function transformState(state: Record<string, unknown>) {
|
function transformState(state: Record<string, unknown>) {
|
||||||
if (
|
if (
|
||||||
!hasProperty(state, 'NetworkController') ||
|
!hasProperty(state, 'NetworkController') ||
|
||||||
!isObject(state.NetworkController) ||
|
!isObject(state.NetworkController)
|
||||||
!hasProperty(state.NetworkController, 'network')
|
|
||||||
) {
|
) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController is ${typeof state.NetworkController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!hasProperty(state.NetworkController, 'network')) {
|
||||||
|
const thePost077SupplementFor084HasNotModifiedState =
|
||||||
|
state.NetworkController.networkId === undefined;
|
||||||
|
if (thePost077SupplementFor084HasNotModifiedState) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController.network is ${typeof state
|
||||||
|
.NetworkController.network}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { migrate, version } from './085';
|
import { migrate, version } from './085';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock('uuid', () => {
|
jest.mock('uuid', () => {
|
||||||
const actual = jest.requireActual('uuid');
|
const actual = jest.requireActual('uuid');
|
||||||
|
|
||||||
@ -10,6 +16,10 @@ jest.mock('uuid', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('migration #85', () => {
|
describe('migration #85', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update the version metadata', async () => {
|
it('should update the version metadata', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -39,6 +49,25 @@ describe('migration #85', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception there is no network controller state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 84,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return state unaltered if there is no network controller previous provider state', async () => {
|
it('should return state unaltered if there is no network controller previous provider state', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
other: 'data',
|
other: 'data',
|
||||||
|
@ -24,6 +24,11 @@ export async function migrate(originalVersionedData: {
|
|||||||
|
|
||||||
function transformState(state: Record<string, unknown>) {
|
function transformState(state: Record<string, unknown>) {
|
||||||
if (!isObject(state.NetworkController)) {
|
if (!isObject(state.NetworkController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController is ${typeof state.NetworkController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { migrate, version } from './086';
|
import { migrate, version } from './086';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock('uuid', () => {
|
jest.mock('uuid', () => {
|
||||||
const actual = jest.requireActual('uuid');
|
const actual = jest.requireActual('uuid');
|
||||||
|
|
||||||
@ -10,6 +16,10 @@ jest.mock('uuid', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('migration #86', () => {
|
describe('migration #86', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update the version metadata', async () => {
|
it('should update the version metadata', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -39,6 +49,25 @@ describe('migration #86', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if there is no network controller state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 85,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return state unaltered if there is no network controller provider state', async () => {
|
it('should return state unaltered if there is no network controller provider state', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
other: 'data',
|
other: 'data',
|
||||||
@ -59,6 +88,52 @@ describe('migration #86', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if there is no network controller provider state and no providerConfig state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
NetworkController: {
|
||||||
|
networkConfigurations: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 85,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController.provider is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not capture an exception if there is no network controller provider state but there is a providerConfig state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
NetworkController: {
|
||||||
|
networkConfigurations: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
providerConfig: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 85,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('should rename the provider config state', async () => {
|
it('should rename the provider config state', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
other: 'data',
|
other: 'data',
|
||||||
|
@ -37,5 +37,24 @@ function transformState(state: Record<string, unknown>) {
|
|||||||
NetworkController: networkControllerState,
|
NetworkController: networkControllerState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!isObject(state.NetworkController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController is ${typeof state.NetworkController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (!hasProperty(state.NetworkController, 'provider')) {
|
||||||
|
const thePost077SupplementFor086HasNotModifiedState =
|
||||||
|
state.NetworkController.providerConfig === undefined;
|
||||||
|
if (thePost077SupplementFor086HasNotModifiedState) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController.provider is ${typeof state
|
||||||
|
.NetworkController.provider}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import { migrate, version } from './087';
|
import { migrate, version } from './087';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
describe('migration #87', () => {
|
describe('migration #87', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update the version metadata', async () => {
|
it('should update the version metadata', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -53,6 +63,65 @@ describe('migration #87', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return state unaltered if TokensController state is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
TokensController: false,
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 86,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStorage = await migrate(oldStorage);
|
||||||
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if TokensController state is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
TokensController: false,
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 86,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokensController is boolean`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not capture an exception if TokensController state is an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
TokensController: {
|
||||||
|
allDetectedTokens: {},
|
||||||
|
allIgnoredTokens: {},
|
||||||
|
allTokens: {},
|
||||||
|
detectedTokens: [],
|
||||||
|
ignoredTokens: [],
|
||||||
|
suggestedAssets: [],
|
||||||
|
tokens: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 86,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('should remove the suggested assets state', async () => {
|
it('should remove the suggested assets state', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
other: 'data',
|
other: 'data',
|
||||||
|
@ -24,6 +24,11 @@ export async function migrate(originalVersionedData: {
|
|||||||
|
|
||||||
function transformState(state: Record<string, unknown>) {
|
function transformState(state: Record<string, unknown>) {
|
||||||
if (!isObject(state.TokensController)) {
|
if (!isObject(state.TokensController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.TokensController is ${typeof state.TokensController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import { migrate } from './088';
|
import { migrate } from './088';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
describe('migration #88', () => {
|
describe('migration #88', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('updates the version metadata', async () => {
|
it('updates the version metadata', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: { version: 87 },
|
meta: { version: 87 },
|
||||||
@ -26,6 +36,24 @@ describe('migration #88', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the NftController property is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokenListController: {},
|
||||||
|
TokensController: {},
|
||||||
|
NftController: false,
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NftController is boolean`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns the state unaltered if the NftController object has no allNftContracts property', async () => {
|
it('returns the state unaltered if the NftController object has no allNftContracts property', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
NftController: {
|
NftController: {
|
||||||
@ -58,6 +86,26 @@ describe('migration #88', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if it NftController.allNftContracts is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokenListController: {},
|
||||||
|
TokensController: {},
|
||||||
|
NftController: {
|
||||||
|
allNftContracts: 'foo',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NftController.allNftContracts is string`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns the state unaltered if any value of the NftController.allNftContracts object is not an object itself', async () => {
|
it('returns the state unaltered if any value of the NftController.allNftContracts object is not an object itself', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
NftController: {
|
NftController: {
|
||||||
@ -324,6 +372,26 @@ describe('migration #88', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if it NftController.allNfts is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokenListController: {},
|
||||||
|
TokensController: {},
|
||||||
|
NftController: {
|
||||||
|
allNfts: 'foo',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NftController.allNfts is string`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns the state unaltered if any value of the NftController.allNfts object is not an object itself', async () => {
|
it('returns the state unaltered if any value of the NftController.allNfts object is not an object itself', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
NftController: {
|
NftController: {
|
||||||
@ -656,6 +724,91 @@ describe('migration #88', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if it has no TokenListController property', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokensController: {},
|
||||||
|
NftController: {
|
||||||
|
allNfts: {
|
||||||
|
'0x111': {
|
||||||
|
'0x10': [
|
||||||
|
{
|
||||||
|
name: 'NFT 1',
|
||||||
|
description: 'Description for NFT 1',
|
||||||
|
image: 'nft1.jpg',
|
||||||
|
standard: 'ERC721',
|
||||||
|
tokenId: '1',
|
||||||
|
address: '0xaaa',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allNftContracts: {
|
||||||
|
'0x111': {
|
||||||
|
'0x10': [
|
||||||
|
{
|
||||||
|
name: 'Contract 1',
|
||||||
|
address: '0xaaa',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokenListController is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the TokenListController property is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokensController: {},
|
||||||
|
NftController: {
|
||||||
|
allNfts: {
|
||||||
|
'0x111': {
|
||||||
|
'0x10': [
|
||||||
|
{
|
||||||
|
name: 'NFT 1',
|
||||||
|
description: 'Description for NFT 1',
|
||||||
|
image: 'nft1.jpg',
|
||||||
|
standard: 'ERC721',
|
||||||
|
tokenId: '1',
|
||||||
|
address: '0xaaa',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allNftContracts: {
|
||||||
|
'0x111': {
|
||||||
|
'0x10': [
|
||||||
|
{
|
||||||
|
name: 'Contract 1',
|
||||||
|
address: '0xaaa',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TokenListController: false,
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokenListController is boolean`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns the state unaltered if the TokenListController object has no tokensChainsCache property', async () => {
|
it('returns the state unaltered if the TokenListController object has no tokensChainsCache property', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
TokenListController: {
|
TokenListController: {
|
||||||
@ -688,6 +841,25 @@ describe('migration #88', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the TokenListController.tokensChainsCache property is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokenListController: {
|
||||||
|
tokensChainsCache: 'foo',
|
||||||
|
},
|
||||||
|
TokensController: {},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokenListController.tokensChainsCache is string`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('rewrites TokenListController.tokensChainsCache so that decimal chain IDs are converted to hex strings', async () => {
|
it('rewrites TokenListController.tokensChainsCache so that decimal chain IDs are converted to hex strings', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: { version: 87 },
|
meta: { version: 87 },
|
||||||
@ -919,6 +1091,39 @@ describe('migration #88', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if it has no TokensController property', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokenListController: {},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokensController is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the TokensController property is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokenListController: {},
|
||||||
|
TokensController: false,
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokensController is boolean`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns the state unaltered if the TokensController object has no allTokens property', async () => {
|
it('returns the state unaltered if the TokensController object has no allTokens property', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
TokensController: {
|
TokensController: {
|
||||||
@ -951,6 +1156,25 @@ describe('migration #88', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the TokensController.allTokens property is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokenListController: {},
|
||||||
|
TokensController: {
|
||||||
|
allTokens: 'foo',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokensController.allTokens is string`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('rewrites TokensController.allTokens so that decimal chain IDs are converted to hex strings', async () => {
|
it('rewrites TokensController.allTokens so that decimal chain IDs are converted to hex strings', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: { version: 87 },
|
meta: { version: 87 },
|
||||||
@ -1163,6 +1387,25 @@ describe('migration #88', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the TokensController.allIgnoredTokens property is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokenListController: {},
|
||||||
|
TokensController: {
|
||||||
|
allIgnoredTokens: 'foo',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokensController.allIgnoredTokens is string`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('rewrites TokensController.allIgnoredTokens so that decimal chain IDs are converted to hex strings', async () => {
|
it('rewrites TokensController.allIgnoredTokens so that decimal chain IDs are converted to hex strings', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: { version: 87 },
|
meta: { version: 87 },
|
||||||
@ -1323,6 +1566,25 @@ describe('migration #88', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the TokensController.allDetectedTokens property is not an object', async () => {
|
||||||
|
const oldData = {
|
||||||
|
TokenListController: {},
|
||||||
|
TokensController: {
|
||||||
|
allDetectedTokens: 'foo',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: { version: 87 },
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokensController.allDetectedTokens is string`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('rewrites TokensController.allDetectedTokens so that decimal chain IDs are converted to hex strings', async () => {
|
it('rewrites TokensController.allDetectedTokens so that decimal chain IDs are converted to hex strings', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: { version: 87 },
|
meta: { version: 87 },
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { hasProperty, Hex, isObject, isStrictHexString } from '@metamask/utils';
|
import { hasProperty, Hex, isObject, isStrictHexString } from '@metamask/utils';
|
||||||
import { BN } from 'ethereumjs-util';
|
import { BN } from 'ethereumjs-util';
|
||||||
import { cloneDeep, mapKeys } from 'lodash';
|
import { cloneDeep, mapKeys } from 'lodash';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
type VersionedData = {
|
type VersionedData = {
|
||||||
meta: { version: number };
|
meta: { version: number };
|
||||||
@ -70,6 +71,16 @@ function migrateData(state: Record<string, unknown>): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (hasProperty(nftControllerState, 'allNftContracts')) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NftController.allNftContracts is ${typeof nftControllerState.allNftContracts}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`typeof state.NftController.allNftContracts is ${typeof nftControllerState.allNftContracts}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate NftController.allNfts
|
// Migrate NftController.allNfts
|
||||||
@ -96,9 +107,25 @@ function migrateData(state: Record<string, unknown>): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (hasProperty(nftControllerState, 'allNfts')) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NftController.allNfts is ${typeof nftControllerState.allNfts}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`typeof state.NftController.allNfts is ${typeof nftControllerState.allNfts}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.NftController = nftControllerState;
|
state.NftController = nftControllerState;
|
||||||
|
} else if (hasProperty(state, 'NftController')) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(`typeof state.NftController is ${typeof state.NftController}`),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.warn(`typeof state.NftController is undefined`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -124,7 +151,24 @@ function migrateData(state: Record<string, unknown>): void {
|
|||||||
tokenListControllerState.tokensChainsCache,
|
tokenListControllerState.tokensChainsCache,
|
||||||
(_, chainId: string) => toHex(chainId),
|
(_, chainId: string) => toHex(chainId),
|
||||||
);
|
);
|
||||||
|
} else if (hasProperty(tokenListControllerState, 'tokensChainsCache')) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.TokenListController.tokensChainsCache is ${typeof state
|
||||||
|
.TokenListController.tokensChainsCache}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`typeof state.TokenListController.tokensChainsCache is undefined`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.TokenListController is ${typeof state.TokenListController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -150,6 +194,16 @@ function migrateData(state: Record<string, unknown>): void {
|
|||||||
allTokens,
|
allTokens,
|
||||||
(_, chainId: string) => toHex(chainId),
|
(_, chainId: string) => toHex(chainId),
|
||||||
);
|
);
|
||||||
|
} else if (hasProperty(tokensControllerState, 'allTokens')) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.TokensController.allTokens is ${typeof tokensControllerState.allTokens}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`typeof state.TokensController.allTokens is ${typeof tokensControllerState.allTokens}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate TokensController.allIgnoredTokens
|
// Migrate TokensController.allIgnoredTokens
|
||||||
@ -169,6 +223,16 @@ function migrateData(state: Record<string, unknown>): void {
|
|||||||
allIgnoredTokens,
|
allIgnoredTokens,
|
||||||
(_, chainId: string) => toHex(chainId),
|
(_, chainId: string) => toHex(chainId),
|
||||||
);
|
);
|
||||||
|
} else if (hasProperty(tokensControllerState, 'allIgnoredTokens')) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.TokensController.allIgnoredTokens is ${typeof tokensControllerState.allIgnoredTokens}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`typeof state.TokensController.allIgnoredTokens is ${typeof tokensControllerState.allIgnoredTokens}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate TokensController.allDetectedTokens
|
// Migrate TokensController.allDetectedTokens
|
||||||
@ -188,9 +252,25 @@ function migrateData(state: Record<string, unknown>): void {
|
|||||||
allDetectedTokens,
|
allDetectedTokens,
|
||||||
(_, chainId: string) => toHex(chainId),
|
(_, chainId: string) => toHex(chainId),
|
||||||
);
|
);
|
||||||
|
} else if (hasProperty(tokensControllerState, 'allDetectedTokens')) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.TokensController.allDetectedTokens is ${typeof tokensControllerState.allDetectedTokens}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`typeof state.TokensController.allDetectedTokens is ${typeof tokensControllerState.allDetectedTokens}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.TokensController = tokensControllerState;
|
state.TokensController = tokensControllerState;
|
||||||
|
} else {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.TokensController is ${typeof state.TokensController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
import { migrate, version } from './089';
|
import { migrate, version } from './089';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
startSession: jest.fn(),
|
||||||
|
endSession: jest.fn(),
|
||||||
|
toggleSession: jest.fn(),
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock('uuid', () => {
|
jest.mock('uuid', () => {
|
||||||
const actual = jest.requireActual('uuid');
|
const actual = jest.requireActual('uuid');
|
||||||
|
|
||||||
@ -10,6 +19,10 @@ jest.mock('uuid', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('migration #89', () => {
|
describe('migration #89', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update the version metadata', async () => {
|
it('should update the version metadata', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -39,6 +52,25 @@ describe('migration #89', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if there is no network controller state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 88,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return state unaltered if there is no network controller providerConfig state', async () => {
|
it('should return state unaltered if there is no network controller providerConfig state', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
other: 'data',
|
other: 'data',
|
||||||
@ -61,6 +93,32 @@ describe('migration #89', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if there is no network controller providerConfig state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
NetworkController: {
|
||||||
|
networkConfigurations: {
|
||||||
|
id1: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 88,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController.providerConfig is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return state unaltered if the providerConfig already has an id', async () => {
|
it('should return state unaltered if the providerConfig already has an id', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
other: 'data',
|
other: 'data',
|
||||||
|
@ -66,6 +66,19 @@ function transformState(state: Record<string, unknown>) {
|
|||||||
...state,
|
...state,
|
||||||
NetworkController: state.NetworkController,
|
NetworkController: state.NetworkController,
|
||||||
};
|
};
|
||||||
|
} else if (!isObject(state.NetworkController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController is ${typeof state.NetworkController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (!isObject(state.NetworkController.providerConfig)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController.providerConfig is ${typeof state
|
||||||
|
.NetworkController.providerConfig}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,17 @@ import { migrate, version } from './090';
|
|||||||
|
|
||||||
const PREVIOUS_VERSION = version - 1;
|
const PREVIOUS_VERSION = version - 1;
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
describe('migration #90', () => {
|
describe('migration #90', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('updates the version metadata', async () => {
|
it('updates the version metadata', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -31,7 +41,7 @@ describe('migration #90', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not change the state if the phishing controller state is invalid', async () => {
|
it('captures an exception if the phishing controller state is invalid', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
version: PREVIOUS_VERSION,
|
version: PREVIOUS_VERSION,
|
||||||
@ -39,9 +49,12 @@ describe('migration #90', () => {
|
|||||||
data: { PhishingController: 'this is not valid' },
|
data: { PhishingController: 'this is not valid' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const newStorage = await migrate(oldStorage);
|
await migrate(oldStorage);
|
||||||
|
|
||||||
expect(newStorage.data).toStrictEqual(oldStorage.data);
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.PhishingController is string`),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not change the state if the listState property does not exist', async () => {
|
it('does not change the state if the listState property does not exist', async () => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { hasProperty, isObject } from '@metamask/utils';
|
import { hasProperty, isObject } from '@metamask/utils';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
export const version = 90;
|
export const version = 90;
|
||||||
|
|
||||||
@ -23,11 +24,22 @@ export async function migrate(originalVersionedData: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function transformState(state: Record<string, unknown>) {
|
function transformState(state: Record<string, unknown>) {
|
||||||
if (
|
if (!hasProperty(state, 'PhishingController')) {
|
||||||
!hasProperty(state, 'PhishingController') ||
|
log.warn(`typeof state.PhishingController is undefined`);
|
||||||
!isObject(state.PhishingController) ||
|
return state;
|
||||||
!hasProperty(state.PhishingController, 'listState')
|
}
|
||||||
) {
|
if (!isObject(state.PhishingController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.PhishingController is ${typeof state.PhishingController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!hasProperty(state.PhishingController, 'listState')) {
|
||||||
|
log.warn(
|
||||||
|
`typeof state.PhishingController.listState is ${typeof state.PhishingController}`,
|
||||||
|
);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { migrate, version } from './091';
|
import { migrate, version } from './091';
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
startSession: jest.fn(),
|
||||||
|
endSession: jest.fn(),
|
||||||
|
toggleSession: jest.fn(),
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock('uuid', () => {
|
jest.mock('uuid', () => {
|
||||||
const actual = jest.requireActual('uuid');
|
const actual = jest.requireActual('uuid');
|
||||||
|
|
||||||
@ -11,6 +20,10 @@ jest.mock('uuid', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('migration #91', () => {
|
describe('migration #91', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update the version metadata', async () => {
|
it('should update the version metadata', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -40,6 +53,25 @@ describe('migration #91', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if there is no network controller state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 90,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.NetworkController is undefined`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return state unaltered if there is no network controller networkConfigurations state', async () => {
|
it('should return state unaltered if there is no network controller networkConfigurations state', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
other: 'data',
|
other: 'data',
|
||||||
@ -60,6 +92,32 @@ describe('migration #91', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture an exception if there is no network controller networkConfigurations state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
NetworkController: {
|
||||||
|
providerConfig: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: 90,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController.networkConfigurations is undefined`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return state unaltered if the networkConfigurations all have a chainId', async () => {
|
it('should return state unaltered if the networkConfigurations all have a chainId', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
other: 'data',
|
other: 'data',
|
||||||
|
@ -50,6 +50,19 @@ function transformState(state: Record<string, unknown>) {
|
|||||||
...state,
|
...state,
|
||||||
NetworkController: state.NetworkController,
|
NetworkController: state.NetworkController,
|
||||||
};
|
};
|
||||||
|
} else if (!isObject(state.NetworkController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController is ${typeof state.NetworkController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (!isObject(state.NetworkController.networkConfigurations)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.NetworkController.networkConfigurations is ${typeof state
|
||||||
|
.NetworkController.networkConfigurations}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
139
app/scripts/migrations/092.1.test.ts
Normal file
139
app/scripts/migrations/092.1.test.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { migrate, version } from './092.1';
|
||||||
|
|
||||||
|
const PREVIOUS_VERSION = 92;
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
startSession: jest.fn(),
|
||||||
|
endSession: jest.fn(),
|
||||||
|
toggleSession: jest.fn(),
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('migration #92.1', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the version metadata', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: PREVIOUS_VERSION,
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStorage = await migrate(oldStorage);
|
||||||
|
expect(newStorage.meta).toStrictEqual({
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return state unaltered if there is no TokenListController state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: PREVIOUS_VERSION,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStorage = await migrate(cloneDeep(oldStorage));
|
||||||
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the TokenListController state is invalid', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: PREVIOUS_VERSION,
|
||||||
|
},
|
||||||
|
data: { TokenListController: 'this is not valid' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.TokenListController is string`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return state unaltered if there is no TokenListController tokensChainsCache state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
TokenListController: {
|
||||||
|
tokenList: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: PREVIOUS_VERSION,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStorage = await migrate(cloneDeep(oldStorage));
|
||||||
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return state unaltered if the tokensChainsCache state is an unexpected type', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
TokenListController: {
|
||||||
|
tokensChainsCache: 'unexpected string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: PREVIOUS_VERSION,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStorage = await migrate(cloneDeep(oldStorage));
|
||||||
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return state unaltered if the tokensChainsCache state is valid', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
TokenListController: {
|
||||||
|
tokensChainsCache: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: PREVIOUS_VERSION,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStorage = await migrate(cloneDeep(oldStorage));
|
||||||
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove undefined tokensChainsCache state', async () => {
|
||||||
|
const oldData = {
|
||||||
|
other: 'data',
|
||||||
|
TokenListController: {
|
||||||
|
tokensChainsCache: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: PREVIOUS_VERSION,
|
||||||
|
},
|
||||||
|
data: oldData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStorage = await migrate(cloneDeep(oldStorage));
|
||||||
|
expect(newStorage.data).toStrictEqual({
|
||||||
|
other: 'data',
|
||||||
|
TokenListController: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
53
app/scripts/migrations/092.1.ts
Normal file
53
app/scripts/migrations/092.1.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { hasProperty, isObject } from '@metamask/utils';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
|
export const version = 92.1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the `TokenListController.tokensChainsCache` state is
|
||||||
|
* `undefined`, and delete it if so.
|
||||||
|
*
|
||||||
|
* This property was accidentally set to `undefined` by an earlier revision of
|
||||||
|
* migration #77 in some cases.
|
||||||
|
*
|
||||||
|
* @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 (!hasProperty(state, 'TokenListController')) {
|
||||||
|
log.warn('Skipping migration, TokenListController state is missing');
|
||||||
|
return state;
|
||||||
|
} else if (!isObject(state.TokenListController)) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.TokenListController is ${typeof state.TokenListController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
} else if (!hasProperty(state.TokenListController, 'tokensChainsCache')) {
|
||||||
|
log.warn(
|
||||||
|
'Skipping migration, TokenListController.tokensChainsCache state is missing',
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.TokenListController.tokensChainsCache === undefined) {
|
||||||
|
delete state.TokenListController.tokensChainsCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
@ -3,7 +3,20 @@ import { migrate, version } from './092';
|
|||||||
|
|
||||||
const PREVIOUS_VERSION = version - 1;
|
const PREVIOUS_VERSION = version - 1;
|
||||||
|
|
||||||
|
const sentryCaptureExceptionMock = jest.fn();
|
||||||
|
|
||||||
|
global.sentry = {
|
||||||
|
startSession: jest.fn(),
|
||||||
|
endSession: jest.fn(),
|
||||||
|
toggleSession: jest.fn(),
|
||||||
|
captureException: sentryCaptureExceptionMock,
|
||||||
|
};
|
||||||
|
|
||||||
describe('migration #92', () => {
|
describe('migration #92', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update the version metadata', async () => {
|
it('should update the version metadata', async () => {
|
||||||
const oldStorage = {
|
const oldStorage = {
|
||||||
meta: {
|
meta: {
|
||||||
@ -33,6 +46,22 @@ describe('migration #92', () => {
|
|||||||
expect(newStorage.data).toStrictEqual(oldData);
|
expect(newStorage.data).toStrictEqual(oldData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures an exception if the phishing controller state is invalid', async () => {
|
||||||
|
const oldStorage = {
|
||||||
|
meta: {
|
||||||
|
version: PREVIOUS_VERSION,
|
||||||
|
},
|
||||||
|
data: { PhishingController: 'this is not valid' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await migrate(oldStorage);
|
||||||
|
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
|
||||||
|
new Error(`typeof state.PhishingController is string`),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return state unaltered if there is no phishing controller last fetched state', async () => {
|
it('should return state unaltered if there is no phishing controller last fetched state', async () => {
|
||||||
const oldData = {
|
const oldData = {
|
||||||
other: 'data',
|
other: 'data',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { hasProperty, isObject } from '@metamask/utils';
|
import { hasProperty, isObject } from '@metamask/utils';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
export const version = 92;
|
export const version = 92;
|
||||||
|
|
||||||
@ -30,6 +31,14 @@ function transformState(state: Record<string, unknown>) {
|
|||||||
) {
|
) {
|
||||||
delete state.PhishingController.stalelistLastFetched;
|
delete state.PhishingController.stalelistLastFetched;
|
||||||
delete state.PhishingController.hotlistLastFetched;
|
delete state.PhishingController.hotlistLastFetched;
|
||||||
|
} else if (hasProperty(state, 'PhishingController')) {
|
||||||
|
global.sentry?.captureException?.(
|
||||||
|
new Error(
|
||||||
|
`typeof state.PhishingController is ${typeof state.PhishingController}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.warn(`typeof state.PhishingController is undefined`);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,7 @@ import * as m089 from './089';
|
|||||||
import * as m090 from './090';
|
import * as m090 from './090';
|
||||||
import * as m091 from './091';
|
import * as m091 from './091';
|
||||||
import * as m092 from './092';
|
import * as m092 from './092';
|
||||||
|
import * as m092point1 from './092.1';
|
||||||
|
|
||||||
const migrations = [
|
const migrations = [
|
||||||
m002,
|
m002,
|
||||||
@ -189,6 +190,7 @@ const migrations = [
|
|||||||
m090,
|
m090,
|
||||||
m091,
|
m091,
|
||||||
m092,
|
m092,
|
||||||
|
m092point1,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default migrations;
|
export default migrations;
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
|
// Disabled to allow setting up initial state hooks first
|
||||||
|
|
||||||
|
// This import sets up global functions required for Sentry to function.
|
||||||
|
// It must be run first in case an error is thrown later during initialization.
|
||||||
|
import './lib/setup-initial-state-hooks';
|
||||||
|
|
||||||
// polyfills
|
// polyfills
|
||||||
import '@formatjs/intl-relativetimeformat/polyfill';
|
import '@formatjs/intl-relativetimeformat/polyfill';
|
||||||
|
|
||||||
// dev only, "react-devtools" import is skipped in prod builds
|
// dev only, "react-devtools" import is skipped in prod builds
|
||||||
import 'react-devtools';
|
import 'react-devtools';
|
||||||
|
|
||||||
// This import sets up a global function required for Sentry to function.
|
|
||||||
// It must be run first in case an error is thrown later during initialization.
|
|
||||||
import './lib/setup-persisted-state-hook';
|
|
||||||
|
|
||||||
import PortStream from 'extension-port-stream';
|
import PortStream from 'extension-port-stream';
|
||||||
import browser from 'webextension-polyfill';
|
import browser from 'webextension-polyfill';
|
||||||
|
|
||||||
|
@ -354,7 +354,7 @@
|
|||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"remove-trailing-slash": "^0.1.1",
|
"remove-trailing-slash": "^0.1.1",
|
||||||
"reselect": "^3.0.1",
|
"reselect": "^3.0.1",
|
||||||
"ses": "^0.18.4",
|
"ses": "^0.18.7",
|
||||||
"single-call-balance-checker-abi": "^1.0.0",
|
"single-call-balance-checker-abi": "^1.0.0",
|
||||||
"unicode-confusables": "^0.1.1",
|
"unicode-confusables": "^0.1.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
* should be included, and a sub-mask implies the property should be further
|
* should be included, and a sub-mask implies the property should be further
|
||||||
* masked according to that sub-mask.
|
* masked according to that sub-mask.
|
||||||
*
|
*
|
||||||
|
* If a property is not found in the last, its type is included instead.
|
||||||
|
*
|
||||||
* @param {object} object - The object to mask
|
* @param {object} object - The object to mask
|
||||||
* @param {Object<object | boolean>} mask - The mask to apply to the object
|
* @param {Object<object | boolean>} mask - The mask to apply to the object
|
||||||
*/
|
*/
|
||||||
@ -16,6 +18,8 @@ export function maskObject(object, mask) {
|
|||||||
state[key] = object[key];
|
state[key] = object[key];
|
||||||
} else if (mask[key]) {
|
} else if (mask[key]) {
|
||||||
state[key] = maskObject(object[key], mask[key]);
|
state[key] = maskObject(object[key], mask[key]);
|
||||||
|
} else {
|
||||||
|
state[key] = typeof object[key];
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -724,6 +724,13 @@ class FixtureBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withBadPreferencesControllerState() {
|
||||||
|
merge(this.fixture.data, {
|
||||||
|
PreferencesController: 5,
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
withTokensControllerERC20() {
|
withTokensControllerERC20() {
|
||||||
merge(this.fixture.data.TokensController, {
|
merge(this.fixture.data.TokensController, {
|
||||||
tokens: [
|
tokens: [
|
||||||
|
@ -1,8 +1,112 @@
|
|||||||
|
const { resolve } = require('path');
|
||||||
|
const { promises: fs } = require('fs');
|
||||||
const { strict: assert } = require('assert');
|
const { strict: assert } = require('assert');
|
||||||
|
const { get, has, set, unset } = require('lodash');
|
||||||
const { Browser } = require('selenium-webdriver');
|
const { Browser } = require('selenium-webdriver');
|
||||||
|
const { format } = require('prettier');
|
||||||
const { convertToHexValue, withFixtures } = require('../helpers');
|
const { convertToHexValue, withFixtures } = require('../helpers');
|
||||||
const FixtureBuilder = require('../fixture-builder');
|
const FixtureBuilder = require('../fixture-builder');
|
||||||
|
|
||||||
|
const maskedBackgroundFields = [
|
||||||
|
'CurrencyController.conversionDate', // This is a timestamp that changes each run
|
||||||
|
];
|
||||||
|
const maskedUiFields = [
|
||||||
|
'metamask.conversionDate', // This is a timestamp that changes each run
|
||||||
|
];
|
||||||
|
|
||||||
|
const removedBackgroundFields = [
|
||||||
|
// This property is timing-dependent
|
||||||
|
'AccountTracker.currentBlockGasLimit',
|
||||||
|
// These properties are set to undefined, causing inconsistencies between Chrome and Firefox
|
||||||
|
'AppStateController.currentPopupId',
|
||||||
|
'AppStateController.timeoutMinutes',
|
||||||
|
];
|
||||||
|
|
||||||
|
const removedUiFields = [
|
||||||
|
// This property is timing-dependent
|
||||||
|
'metamask.currentBlockGasLimit',
|
||||||
|
// These properties are set to undefined, causing inconsistencies between Chrome and Firefox
|
||||||
|
'metamask.currentPopupId',
|
||||||
|
'metamask.timeoutMinutes',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform background state to make it consistent between test runs.
|
||||||
|
*
|
||||||
|
* @param {unknown} data - The data to transform
|
||||||
|
*/
|
||||||
|
function transformBackgroundState(data) {
|
||||||
|
for (const field of maskedBackgroundFields) {
|
||||||
|
if (has(data, field)) {
|
||||||
|
set(data, field, typeof get(data, field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const field of removedBackgroundFields) {
|
||||||
|
if (has(data, field)) {
|
||||||
|
unset(data, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform UI state to make it consistent between test runs.
|
||||||
|
*
|
||||||
|
* @param {unknown} data - The data to transform
|
||||||
|
*/
|
||||||
|
function transformUiState(data) {
|
||||||
|
for (const field of maskedUiFields) {
|
||||||
|
if (has(data, field)) {
|
||||||
|
set(data, field, typeof get(data, field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const field of removedUiFields) {
|
||||||
|
if (has(data, field)) {
|
||||||
|
unset(data, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the data provided matches the snapshot.
|
||||||
|
*
|
||||||
|
* @param {object }args - Function arguments.
|
||||||
|
* @param {any} args.data - The data to compare with the snapshot.
|
||||||
|
* @param {string} args.snapshot - The name of the snapshot.
|
||||||
|
* @param {boolean} [args.update] - Whether to update the snapshot if it doesn't match.
|
||||||
|
*/
|
||||||
|
async function matchesSnapshot({
|
||||||
|
data,
|
||||||
|
snapshot,
|
||||||
|
update = process.env.UPDATE_SNAPSHOTS === 'true',
|
||||||
|
}) {
|
||||||
|
const snapshotPath = resolve(__dirname, `./state-snapshots/${snapshot}.json`);
|
||||||
|
const rawSnapshotData = await fs.readFile(snapshotPath, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
const snapshotData = JSON.parse(rawSnapshotData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert.deepStrictEqual(data, snapshotData);
|
||||||
|
} catch (error) {
|
||||||
|
if (update && error instanceof assert.AssertionError) {
|
||||||
|
const stringifiedData = JSON.stringify(data);
|
||||||
|
// filepath specified so that Prettier can infer which parser to use
|
||||||
|
// from the file extension
|
||||||
|
const formattedData = format(stringifiedData, {
|
||||||
|
filepath: 'something.json',
|
||||||
|
});
|
||||||
|
await fs.writeFile(snapshotPath, formattedData, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
console.log(`Snapshot '${snapshot}' updated`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('Sentry errors', function () {
|
describe('Sentry errors', function () {
|
||||||
const migrationError =
|
const migrationError =
|
||||||
process.env.SELENIUM_BROWSER === Browser.CHROME
|
process.env.SELENIUM_BROWSER === Browser.CHROME
|
||||||
@ -20,6 +124,18 @@ describe('Sentry errors', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function mockSentryInvariantMigrationError(mockServer) {
|
||||||
|
return await mockServer
|
||||||
|
.forPost('https://sentry.io/api/0000000/envelope/')
|
||||||
|
.withBodyIncluding('typeof state.PreferencesController is number')
|
||||||
|
.thenCallback(() => {
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
json: {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function mockSentryTestError(mockServer) {
|
async function mockSentryTestError(mockServer) {
|
||||||
return await mockServer
|
return await mockServer
|
||||||
.forPost('https://sentry.io/api/0000000/envelope/')
|
.forPost('https://sentry.io/api/0000000/envelope/')
|
||||||
@ -41,8 +157,8 @@ describe('Sentry errors', function () {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('before initialization', function () {
|
describe('before initialization, after opting out of metrics', function () {
|
||||||
it('should NOT send error events when participateInMetaMetrics is false', async function () {
|
it('should NOT send error events in the background', async function () {
|
||||||
await withFixtures(
|
await withFixtures(
|
||||||
{
|
{
|
||||||
fixtures: {
|
fixtures: {
|
||||||
@ -73,7 +189,43 @@ describe('Sentry errors', function () {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should send error events', async function () {
|
|
||||||
|
it('should NOT send error events in the UI', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
fixtures: new FixtureBuilder()
|
||||||
|
.withMetaMetricsController({
|
||||||
|
metaMetricsId: null,
|
||||||
|
participateInMetaMetrics: false,
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
testSpecificMock: mockSentryTestError,
|
||||||
|
},
|
||||||
|
async ({ driver, mockedEndpoint }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
await driver.findElement('#password');
|
||||||
|
// Erase `getSentryAppState` hook, simulating a "before initialization" state
|
||||||
|
await driver.executeScript(
|
||||||
|
'window.stateHooks.getSentryAppState = undefined',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for Sentry request
|
||||||
|
await driver.delay(3000);
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
assert.ok(
|
||||||
|
isPending,
|
||||||
|
'A request to sentry was sent when it should not have been',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('before initialization, after opting into metrics', function () {
|
||||||
|
it('should send error events in background', async function () {
|
||||||
await withFixtures(
|
await withFixtures(
|
||||||
{
|
{
|
||||||
fixtures: {
|
fixtures: {
|
||||||
@ -112,40 +264,110 @@ describe('Sentry errors', function () {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('after initialization', function () {
|
it('should capture background application state', async function () {
|
||||||
it('should NOT send error events when participateInMetaMetrics is false', async function () {
|
|
||||||
await withFixtures(
|
await withFixtures(
|
||||||
{
|
{
|
||||||
fixtures: new FixtureBuilder()
|
fixtures: {
|
||||||
.withMetaMetricsController({
|
...new FixtureBuilder()
|
||||||
metaMetricsId: null,
|
.withMetaMetricsController({
|
||||||
participateInMetaMetrics: false,
|
metaMetricsId: 'fake-metrics-id',
|
||||||
})
|
participateInMetaMetrics: true,
|
||||||
.build(),
|
})
|
||||||
|
.build(),
|
||||||
|
// Intentionally corrupt state to trigger migration error during initialization
|
||||||
|
meta: undefined,
|
||||||
|
},
|
||||||
ganacheOptions,
|
ganacheOptions,
|
||||||
title: this.test.title,
|
title: this.test.title,
|
||||||
failOnConsoleError: false,
|
failOnConsoleError: false,
|
||||||
testSpecificMock: mockSentryTestError,
|
testSpecificMock: mockSentryMigratorError,
|
||||||
},
|
},
|
||||||
async ({ driver, mockedEndpoint }) => {
|
async ({ driver, mockedEndpoint }) => {
|
||||||
await driver.navigate();
|
await driver.navigate();
|
||||||
await driver.fill('#password', 'correct horse battery staple');
|
|
||||||
await driver.press('#password', driver.Key.ENTER);
|
|
||||||
// Trigger error
|
|
||||||
driver.executeScript('window.stateHooks.throwTestError()');
|
|
||||||
driver.delay(3000);
|
|
||||||
// Wait for Sentry request
|
// Wait for Sentry request
|
||||||
const isPending = await mockedEndpoint.isPending();
|
await driver.wait(async () => {
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
return isPending === false;
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
|
||||||
|
const mockTextBody = mockedRequest.body.text.split('\n');
|
||||||
|
const mockJsonBody = JSON.parse(mockTextBody[2]);
|
||||||
|
const appState = mockJsonBody?.extra?.appState;
|
||||||
|
assert.deepStrictEqual(Object.keys(appState), [
|
||||||
|
'browser',
|
||||||
|
'version',
|
||||||
|
'persistedState',
|
||||||
|
]);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
isPending,
|
typeof appState?.browser === 'string' &&
|
||||||
'A request to sentry was sent when it should not have been',
|
appState?.browser.length > 0,
|
||||||
|
'Invalid browser state',
|
||||||
);
|
);
|
||||||
|
assert.ok(
|
||||||
|
typeof appState?.version === 'string' &&
|
||||||
|
appState?.version.length > 0,
|
||||||
|
'Invalid version state',
|
||||||
|
);
|
||||||
|
await matchesSnapshot({
|
||||||
|
data: transformBackgroundState(appState.persistedState),
|
||||||
|
snapshot: 'errors-before-init-opt-in-background-state',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should send error events', async function () {
|
|
||||||
|
it('should capture migration log breadcrumbs when there is an invariant state error in a migration', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
fixtures: {
|
||||||
|
...new FixtureBuilder()
|
||||||
|
.withMetaMetricsController({
|
||||||
|
metaMetricsId: 'fake-metrics-id',
|
||||||
|
participateInMetaMetrics: true,
|
||||||
|
})
|
||||||
|
.withBadPreferencesControllerState()
|
||||||
|
.build(),
|
||||||
|
},
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
testSpecificMock: mockSentryInvariantMigrationError,
|
||||||
|
},
|
||||||
|
async ({ driver, mockedEndpoint }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
|
||||||
|
// Wait for Sentry request
|
||||||
|
await driver.wait(async () => {
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
return isPending === false;
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
|
||||||
|
const mockTextBody = mockedRequest.body.text.split('\n');
|
||||||
|
const mockJsonBody = JSON.parse(mockTextBody[2]);
|
||||||
|
const breadcrumbs = mockJsonBody?.breadcrumbs ?? [];
|
||||||
|
const migrationLogBreadcrumbs = breadcrumbs.filter((breadcrumb) => {
|
||||||
|
return breadcrumb.message?.match(/Running migration \d+/u);
|
||||||
|
});
|
||||||
|
const migrationLogMessages = migrationLogBreadcrumbs.map(
|
||||||
|
(breadcrumb) =>
|
||||||
|
breadcrumb.message.match(/(Running migration \d+)/u)[1],
|
||||||
|
);
|
||||||
|
const firstMigrationLog = migrationLogMessages[0];
|
||||||
|
const lastMigrationLog =
|
||||||
|
migrationLogMessages[migrationLogMessages.length - 1];
|
||||||
|
|
||||||
|
assert.equal(migrationLogMessages.length, 8);
|
||||||
|
assert.equal(firstMigrationLog, 'Running migration 75');
|
||||||
|
assert.equal(lastMigrationLog, 'Running migration 82');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send error events in UI', async function () {
|
||||||
await withFixtures(
|
await withFixtures(
|
||||||
{
|
{
|
||||||
fixtures: new FixtureBuilder()
|
fixtures: new FixtureBuilder()
|
||||||
@ -161,21 +383,193 @@ describe('Sentry errors', function () {
|
|||||||
},
|
},
|
||||||
async ({ driver, mockedEndpoint }) => {
|
async ({ driver, mockedEndpoint }) => {
|
||||||
await driver.navigate();
|
await driver.navigate();
|
||||||
await driver.fill('#password', 'correct horse battery staple');
|
await driver.findElement('#password');
|
||||||
await driver.press('#password', driver.Key.ENTER);
|
// Erase `getSentryAppState` hook, simulating a "before initialization" state
|
||||||
|
await driver.executeScript(
|
||||||
|
'window.stateHooks.getSentryAppState = undefined',
|
||||||
|
);
|
||||||
|
|
||||||
// Trigger error
|
// Trigger error
|
||||||
driver.executeScript('window.stateHooks.throwTestError()');
|
await driver.executeScript('window.stateHooks.throwTestError()');
|
||||||
|
|
||||||
// Wait for Sentry request
|
// Wait for Sentry request
|
||||||
await driver.wait(async () => {
|
await driver.wait(async () => {
|
||||||
const isPending = await mockedEndpoint.isPending();
|
const isPending = await mockedEndpoint.isPending();
|
||||||
return isPending === false;
|
return isPending === false;
|
||||||
}, 10000);
|
}, 3000);
|
||||||
|
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
|
||||||
|
const mockTextBody = mockedRequest.body.text.split('\n');
|
||||||
|
const mockJsonBody = JSON.parse(mockTextBody[2]);
|
||||||
|
const { level } = mockJsonBody;
|
||||||
|
const [{ type, value }] = mockJsonBody.exception.values;
|
||||||
|
// Verify request
|
||||||
|
assert.equal(type, 'TestError');
|
||||||
|
assert.equal(value, 'Test Error');
|
||||||
|
assert.equal(level, 'error');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should capture UI application state', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
fixtures: new FixtureBuilder()
|
||||||
|
.withMetaMetricsController({
|
||||||
|
metaMetricsId: 'fake-metrics-id',
|
||||||
|
participateInMetaMetrics: true,
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
testSpecificMock: mockSentryTestError,
|
||||||
|
},
|
||||||
|
async ({ driver, mockedEndpoint }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
await driver.findElement('#password');
|
||||||
|
// Erase `getSentryAppState` hook, simulating a "before initialization" state
|
||||||
|
await driver.executeScript(
|
||||||
|
'window.stateHooks.getSentryAppState = undefined',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger error
|
||||||
|
await driver.executeScript('window.stateHooks.throwTestError()');
|
||||||
|
|
||||||
|
// Wait for Sentry request
|
||||||
|
await driver.wait(async () => {
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
return isPending === false;
|
||||||
|
}, 3000);
|
||||||
|
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
|
||||||
|
const mockTextBody = mockedRequest.body.text.split('\n');
|
||||||
|
const mockJsonBody = JSON.parse(mockTextBody[2]);
|
||||||
|
const appState = mockJsonBody?.extra?.appState;
|
||||||
|
assert.deepStrictEqual(Object.keys(appState), [
|
||||||
|
'browser',
|
||||||
|
'version',
|
||||||
|
'persistedState',
|
||||||
|
]);
|
||||||
|
assert.ok(
|
||||||
|
typeof appState?.browser === 'string' &&
|
||||||
|
appState?.browser.length > 0,
|
||||||
|
'Invalid browser state',
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
typeof appState?.version === 'string' &&
|
||||||
|
appState?.version.length > 0,
|
||||||
|
'Invalid version state',
|
||||||
|
);
|
||||||
|
await matchesSnapshot({
|
||||||
|
data: transformBackgroundState(appState.persistedState),
|
||||||
|
snapshot: 'errors-before-init-opt-in-ui-state',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after initialization, after opting out of metrics', function () {
|
||||||
|
it('should NOT send error events in the background', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
fixtures: new FixtureBuilder()
|
||||||
|
.withMetaMetricsController({
|
||||||
|
metaMetricsId: null,
|
||||||
|
participateInMetaMetrics: false,
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
testSpecificMock: mockSentryTestError,
|
||||||
|
},
|
||||||
|
async ({ driver, mockedEndpoint }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
await driver.findElement('#password');
|
||||||
|
|
||||||
|
// Trigger error
|
||||||
|
await driver.executeScript(
|
||||||
|
'window.stateHooks.throwTestBackgroundError()',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for Sentry request
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
assert.ok(
|
||||||
|
isPending,
|
||||||
|
'A request to sentry was sent when it should not have been',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT send error events in the UI', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
fixtures: new FixtureBuilder()
|
||||||
|
.withMetaMetricsController({
|
||||||
|
metaMetricsId: null,
|
||||||
|
participateInMetaMetrics: false,
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
testSpecificMock: mockSentryTestError,
|
||||||
|
},
|
||||||
|
async ({ driver, mockedEndpoint }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
await driver.findElement('#password');
|
||||||
|
|
||||||
|
// Trigger error
|
||||||
|
await driver.executeScript('window.stateHooks.throwTestError()');
|
||||||
|
|
||||||
|
// Wait for Sentry request
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
assert.ok(
|
||||||
|
isPending,
|
||||||
|
'A request to sentry was sent when it should not have been',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after initialization, after opting into metrics', function () {
|
||||||
|
it('should send error events in background', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
fixtures: new FixtureBuilder()
|
||||||
|
.withMetaMetricsController({
|
||||||
|
metaMetricsId: 'fake-metrics-id',
|
||||||
|
participateInMetaMetrics: true,
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
testSpecificMock: mockSentryTestError,
|
||||||
|
},
|
||||||
|
async ({ driver, mockedEndpoint }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
await driver.findElement('#password');
|
||||||
|
|
||||||
|
// Trigger error
|
||||||
|
await driver.executeScript(
|
||||||
|
'window.stateHooks.throwTestBackgroundError()',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for Sentry request
|
||||||
|
await driver.wait(async () => {
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
return isPending === false;
|
||||||
|
}, 3000);
|
||||||
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
|
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
|
||||||
const mockTextBody = mockedRequest.body.text.split('\n');
|
const mockTextBody = mockedRequest.body.text.split('\n');
|
||||||
const mockJsonBody = JSON.parse(mockTextBody[2]);
|
const mockJsonBody = JSON.parse(mockTextBody[2]);
|
||||||
const { level, extra } = mockJsonBody;
|
const { level, extra } = mockJsonBody;
|
||||||
const [{ type, value }] = mockJsonBody.exception.values;
|
const [{ type, value }] = mockJsonBody.exception.values;
|
||||||
const { participateInMetaMetrics } = extra.appState.store.metamask;
|
const { participateInMetaMetrics } =
|
||||||
|
extra.appState.state.MetaMetricsController;
|
||||||
// Verify request
|
// Verify request
|
||||||
assert.equal(type, 'TestError');
|
assert.equal(type, 'TestError');
|
||||||
assert.equal(value, 'Test Error');
|
assert.equal(value, 'Test Error');
|
||||||
@ -184,5 +578,154 @@ describe('Sentry errors', function () {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should capture background application state', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
fixtures: new FixtureBuilder()
|
||||||
|
.withMetaMetricsController({
|
||||||
|
metaMetricsId: 'fake-metrics-id',
|
||||||
|
participateInMetaMetrics: true,
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
testSpecificMock: mockSentryTestError,
|
||||||
|
},
|
||||||
|
async ({ driver, mockedEndpoint }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
await driver.findElement('#password');
|
||||||
|
|
||||||
|
// Trigger error
|
||||||
|
await driver.executeScript(
|
||||||
|
'window.stateHooks.throwTestBackgroundError()',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for Sentry request
|
||||||
|
await driver.wait(async () => {
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
return isPending === false;
|
||||||
|
}, 3000);
|
||||||
|
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
|
||||||
|
const mockTextBody = mockedRequest.body.text.split('\n');
|
||||||
|
const mockJsonBody = JSON.parse(mockTextBody[2]);
|
||||||
|
const appState = mockJsonBody?.extra?.appState;
|
||||||
|
assert.deepStrictEqual(Object.keys(appState), [
|
||||||
|
'browser',
|
||||||
|
'version',
|
||||||
|
'state',
|
||||||
|
]);
|
||||||
|
assert.ok(
|
||||||
|
typeof appState?.browser === 'string' &&
|
||||||
|
appState?.browser.length > 0,
|
||||||
|
'Invalid browser state',
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
typeof appState?.version === 'string' &&
|
||||||
|
appState?.version.length > 0,
|
||||||
|
'Invalid version state',
|
||||||
|
);
|
||||||
|
await matchesSnapshot({
|
||||||
|
data: transformBackgroundState(appState.state),
|
||||||
|
snapshot: 'errors-after-init-opt-in-background-state',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send error events in UI', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
fixtures: new FixtureBuilder()
|
||||||
|
.withMetaMetricsController({
|
||||||
|
metaMetricsId: 'fake-metrics-id',
|
||||||
|
participateInMetaMetrics: true,
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
testSpecificMock: mockSentryTestError,
|
||||||
|
},
|
||||||
|
async ({ driver, mockedEndpoint }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
await driver.findElement('#password');
|
||||||
|
|
||||||
|
// Trigger error
|
||||||
|
await driver.executeScript('window.stateHooks.throwTestError()');
|
||||||
|
|
||||||
|
// Wait for Sentry request
|
||||||
|
await driver.wait(async () => {
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
return isPending === false;
|
||||||
|
}, 3000);
|
||||||
|
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
|
||||||
|
const mockTextBody = mockedRequest.body.text.split('\n');
|
||||||
|
const mockJsonBody = JSON.parse(mockTextBody[2]);
|
||||||
|
const { level, extra } = mockJsonBody;
|
||||||
|
const [{ type, value }] = mockJsonBody.exception.values;
|
||||||
|
const { participateInMetaMetrics } = extra.appState.state.metamask;
|
||||||
|
// Verify request
|
||||||
|
assert.equal(type, 'TestError');
|
||||||
|
assert.equal(value, 'Test Error');
|
||||||
|
assert.equal(level, 'error');
|
||||||
|
assert.equal(participateInMetaMetrics, true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should capture UI application state', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
fixtures: new FixtureBuilder()
|
||||||
|
.withMetaMetricsController({
|
||||||
|
metaMetricsId: 'fake-metrics-id',
|
||||||
|
participateInMetaMetrics: true,
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
testSpecificMock: mockSentryTestError,
|
||||||
|
},
|
||||||
|
async ({ driver, mockedEndpoint }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
await driver.findElement('#password');
|
||||||
|
|
||||||
|
// Trigger error
|
||||||
|
await driver.executeScript('window.stateHooks.throwTestError()');
|
||||||
|
|
||||||
|
// Wait for Sentry request
|
||||||
|
await driver.wait(async () => {
|
||||||
|
const isPending = await mockedEndpoint.isPending();
|
||||||
|
return isPending === false;
|
||||||
|
}, 3000);
|
||||||
|
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
|
||||||
|
const mockTextBody = mockedRequest.body.text.split('\n');
|
||||||
|
const mockJsonBody = JSON.parse(mockTextBody[2]);
|
||||||
|
const appState = mockJsonBody?.extra?.appState;
|
||||||
|
assert.deepStrictEqual(Object.keys(appState), [
|
||||||
|
'browser',
|
||||||
|
'version',
|
||||||
|
'state',
|
||||||
|
]);
|
||||||
|
assert.ok(
|
||||||
|
typeof appState?.browser === 'string' &&
|
||||||
|
appState?.browser.length > 0,
|
||||||
|
'Invalid browser state',
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
typeof appState?.version === 'string' &&
|
||||||
|
appState?.version.length > 0,
|
||||||
|
'Invalid version state',
|
||||||
|
);
|
||||||
|
await matchesSnapshot({
|
||||||
|
data: transformUiState(appState.state),
|
||||||
|
snapshot: 'errors-after-init-opt-in-ui-state',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
"AccountTracker": { "accounts": "object" },
|
||||||
|
"AddressBookController": "object",
|
||||||
|
"AlertController": {
|
||||||
|
"alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true },
|
||||||
|
"unconnectedAccountAlertShownOrigins": "object",
|
||||||
|
"web3ShimUsageOrigins": "object"
|
||||||
|
},
|
||||||
|
"AnnouncementController": "object",
|
||||||
|
"AppMetadataController": {
|
||||||
|
"currentAppVersion": "10.34.5",
|
||||||
|
"previousAppVersion": "",
|
||||||
|
"previousMigrationVersion": 0,
|
||||||
|
"currentMigrationVersion": 92.1
|
||||||
|
},
|
||||||
|
"AppStateController": {
|
||||||
|
"connectedStatusPopoverHasBeenShown": true,
|
||||||
|
"defaultHomeActiveTabName": null,
|
||||||
|
"browserEnvironment": "object",
|
||||||
|
"popupGasPollTokens": "object",
|
||||||
|
"notificationGasPollTokens": "object",
|
||||||
|
"fullScreenGasPollTokens": "object",
|
||||||
|
"recoveryPhraseReminderHasBeenShown": "boolean",
|
||||||
|
"recoveryPhraseReminderLastShown": "number",
|
||||||
|
"outdatedBrowserWarningLastShown": "number",
|
||||||
|
"nftsDetectionNoticeDismissed": "boolean",
|
||||||
|
"showTestnetMessageInDropdown": "boolean",
|
||||||
|
"showBetaHeader": "boolean",
|
||||||
|
"showProductTour": "boolean",
|
||||||
|
"trezorModel": "object",
|
||||||
|
"nftsDropdownState": "object",
|
||||||
|
"termsOfUseLastAgreed": "number",
|
||||||
|
"qrHardware": "object",
|
||||||
|
"usedNetworks": "object",
|
||||||
|
"snapsInstallPrivacyWarningShown": "boolean",
|
||||||
|
"serviceWorkerLastActiveTime": "number"
|
||||||
|
},
|
||||||
|
"ApprovalController": "object",
|
||||||
|
"BackupController": "undefined",
|
||||||
|
"CachedBalancesController": "object",
|
||||||
|
"CurrencyController": {
|
||||||
|
"conversionDate": "number",
|
||||||
|
"conversionRate": 1700,
|
||||||
|
"nativeCurrency": "ETH",
|
||||||
|
"currentCurrency": "usd",
|
||||||
|
"pendingCurrentCurrency": "object",
|
||||||
|
"pendingNativeCurrency": "object",
|
||||||
|
"usdConversionRate": "number"
|
||||||
|
},
|
||||||
|
"DecryptMessageController": {
|
||||||
|
"unapprovedDecryptMsgs": "object",
|
||||||
|
"unapprovedDecryptMsgCount": 0
|
||||||
|
},
|
||||||
|
"EncryptionPublicKeyController": {
|
||||||
|
"unapprovedEncryptionPublicKeyMsgs": "object",
|
||||||
|
"unapprovedEncryptionPublicKeyMsgCount": 0
|
||||||
|
},
|
||||||
|
"EnsController": "object",
|
||||||
|
"GasFeeController": "object",
|
||||||
|
"IncomingTransactionsController": {
|
||||||
|
"incomingTransactions": "object",
|
||||||
|
"incomingTxLastFetchedBlockByChainId": {
|
||||||
|
"0x1": null,
|
||||||
|
"0xe708": null,
|
||||||
|
"0x5": null,
|
||||||
|
"0xaa36a7": null,
|
||||||
|
"0xe704": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"KeyringController": {
|
||||||
|
"isUnlocked": false,
|
||||||
|
"keyringTypes": "object",
|
||||||
|
"keyrings": "object",
|
||||||
|
"encryptionKey": "object"
|
||||||
|
},
|
||||||
|
"MetaMetricsController": {
|
||||||
|
"participateInMetaMetrics": true,
|
||||||
|
"metaMetricsId": "fake-metrics-id",
|
||||||
|
"eventsBeforeMetricsOptIn": "object",
|
||||||
|
"traits": "object",
|
||||||
|
"fragments": "object",
|
||||||
|
"segmentApiCalls": "object",
|
||||||
|
"previousUserTraits": "object"
|
||||||
|
},
|
||||||
|
"NetworkController": {
|
||||||
|
"networkId": "1337",
|
||||||
|
"networkStatus": "available",
|
||||||
|
"providerConfig": {
|
||||||
|
"chainId": "string",
|
||||||
|
"nickname": "Localhost 8545",
|
||||||
|
"rpcPrefs": "object",
|
||||||
|
"rpcUrl": "string",
|
||||||
|
"ticker": "ETH",
|
||||||
|
"type": "rpc",
|
||||||
|
"id": "string"
|
||||||
|
},
|
||||||
|
"networkDetails": "object",
|
||||||
|
"networkConfigurations": "object"
|
||||||
|
},
|
||||||
|
"NftController": "object",
|
||||||
|
"OnboardingController": {
|
||||||
|
"seedPhraseBackedUp": true,
|
||||||
|
"firstTimeFlowType": "import",
|
||||||
|
"completedOnboarding": true,
|
||||||
|
"onboardingTabs": "object"
|
||||||
|
},
|
||||||
|
"PermissionController": "object",
|
||||||
|
"PermissionLogController": "object",
|
||||||
|
"PreferencesController": {
|
||||||
|
"useBlockie": false,
|
||||||
|
"useNonceField": false,
|
||||||
|
"usePhishDetect": true,
|
||||||
|
"dismissSeedBackUpReminder": "boolean",
|
||||||
|
"disabledRpcMethodPreferences": "object",
|
||||||
|
"useMultiAccountBalanceChecker": "boolean",
|
||||||
|
"useTokenDetection": "boolean",
|
||||||
|
"useNftDetection": "boolean",
|
||||||
|
"useCurrencyRateCheck": "boolean",
|
||||||
|
"openSeaEnabled": "boolean",
|
||||||
|
"advancedGasFee": "object",
|
||||||
|
"featureFlags": { "showIncomingTransactions": true },
|
||||||
|
"knownMethodData": "object",
|
||||||
|
"currentLocale": "en",
|
||||||
|
"identities": "object",
|
||||||
|
"lostIdentities": "object",
|
||||||
|
"forgottenPassword": false,
|
||||||
|
"preferences": {
|
||||||
|
"hideZeroBalanceTokens": false,
|
||||||
|
"showFiatInTestnets": false,
|
||||||
|
"showTestNetworks": false,
|
||||||
|
"useNativeCurrencyAsPrimaryCurrency": true
|
||||||
|
},
|
||||||
|
"ipfsGateway": "dweb.link",
|
||||||
|
"infuraBlocked": "boolean",
|
||||||
|
"ledgerTransportType": "string",
|
||||||
|
"snapRegistryList": "object",
|
||||||
|
"transactionSecurityCheckEnabled": "boolean",
|
||||||
|
"theme": "string",
|
||||||
|
"isLineaMainnetReleased": "boolean",
|
||||||
|
"selectedAddress": "string"
|
||||||
|
},
|
||||||
|
"SignatureController": {
|
||||||
|
"unapprovedMsgs": "object",
|
||||||
|
"unapprovedPersonalMsgs": "object",
|
||||||
|
"unapprovedTypedMessages": "object",
|
||||||
|
"unapprovedMsgCount": 0,
|
||||||
|
"unapprovedPersonalMsgCount": 0,
|
||||||
|
"unapprovedTypedMessagesCount": 0
|
||||||
|
},
|
||||||
|
"SmartTransactionsController": "object",
|
||||||
|
"SubjectMetadataController": "object",
|
||||||
|
"SwapsController": "object",
|
||||||
|
"TokenListController": "object",
|
||||||
|
"TokenRatesController": "object",
|
||||||
|
"TokensController": "object",
|
||||||
|
"TxController": "object"
|
||||||
|
}
|
@ -0,0 +1,165 @@
|
|||||||
|
{
|
||||||
|
"DNS": "object",
|
||||||
|
"activeTab": "object",
|
||||||
|
"appState": "object",
|
||||||
|
"confirmTransaction": "object",
|
||||||
|
"gas": { "customData": { "price": null, "limit": null } },
|
||||||
|
"history": { "mostRecentOverviewPage": "/" },
|
||||||
|
"invalidCustomNetwork": "object",
|
||||||
|
"localeMessages": "object",
|
||||||
|
"metamask": {
|
||||||
|
"isInitialized": true,
|
||||||
|
"isUnlocked": false,
|
||||||
|
"isAccountMenuOpen": false,
|
||||||
|
"isNetworkMenuOpen": "boolean",
|
||||||
|
"identities": "object",
|
||||||
|
"unapprovedTxs": "object",
|
||||||
|
"networkConfigurations": "object",
|
||||||
|
"addressBook": "object",
|
||||||
|
"contractExchangeRates": "object",
|
||||||
|
"pendingTokens": "object",
|
||||||
|
"customNonceValue": "",
|
||||||
|
"useBlockie": false,
|
||||||
|
"featureFlags": { "showIncomingTransactions": true },
|
||||||
|
"welcomeScreenSeen": false,
|
||||||
|
"currentLocale": "en",
|
||||||
|
"preferences": {
|
||||||
|
"hideZeroBalanceTokens": false,
|
||||||
|
"showFiatInTestnets": false,
|
||||||
|
"showTestNetworks": false,
|
||||||
|
"useNativeCurrencyAsPrimaryCurrency": true
|
||||||
|
},
|
||||||
|
"firstTimeFlowType": "import",
|
||||||
|
"completedOnboarding": true,
|
||||||
|
"knownMethodData": "object",
|
||||||
|
"participateInMetaMetrics": true,
|
||||||
|
"nextNonce": null,
|
||||||
|
"conversionRate": 1300,
|
||||||
|
"nativeCurrency": "ETH",
|
||||||
|
"connectedStatusPopoverHasBeenShown": true,
|
||||||
|
"defaultHomeActiveTabName": null,
|
||||||
|
"browserEnvironment": "object",
|
||||||
|
"popupGasPollTokens": "object",
|
||||||
|
"notificationGasPollTokens": "object",
|
||||||
|
"fullScreenGasPollTokens": "object",
|
||||||
|
"recoveryPhraseReminderHasBeenShown": "boolean",
|
||||||
|
"recoveryPhraseReminderLastShown": "number",
|
||||||
|
"outdatedBrowserWarningLastShown": "number",
|
||||||
|
"nftsDetectionNoticeDismissed": "boolean",
|
||||||
|
"showTestnetMessageInDropdown": "boolean",
|
||||||
|
"showBetaHeader": "boolean",
|
||||||
|
"showProductTour": "boolean",
|
||||||
|
"trezorModel": "object",
|
||||||
|
"nftsDropdownState": "object",
|
||||||
|
"termsOfUseLastAgreed": "number",
|
||||||
|
"qrHardware": "object",
|
||||||
|
"usedNetworks": "object",
|
||||||
|
"snapsInstallPrivacyWarningShown": "boolean",
|
||||||
|
"serviceWorkerLastActiveTime": "number",
|
||||||
|
"currentAppVersion": "10.34.5",
|
||||||
|
"previousAppVersion": "",
|
||||||
|
"previousMigrationVersion": 0,
|
||||||
|
"currentMigrationVersion": 92.1,
|
||||||
|
"networkId": "1337",
|
||||||
|
"networkStatus": "available",
|
||||||
|
"providerConfig": {
|
||||||
|
"chainId": "string",
|
||||||
|
"nickname": "Localhost 8545",
|
||||||
|
"rpcPrefs": "object",
|
||||||
|
"rpcUrl": "string",
|
||||||
|
"ticker": "ETH",
|
||||||
|
"type": "rpc",
|
||||||
|
"id": "string"
|
||||||
|
},
|
||||||
|
"networkDetails": "object",
|
||||||
|
"cachedBalances": "object",
|
||||||
|
"keyringTypes": "object",
|
||||||
|
"keyrings": "object",
|
||||||
|
"encryptionKey": "object",
|
||||||
|
"useNonceField": false,
|
||||||
|
"usePhishDetect": true,
|
||||||
|
"dismissSeedBackUpReminder": "boolean",
|
||||||
|
"disabledRpcMethodPreferences": "object",
|
||||||
|
"useMultiAccountBalanceChecker": "boolean",
|
||||||
|
"useTokenDetection": "boolean",
|
||||||
|
"useNftDetection": "boolean",
|
||||||
|
"useCurrencyRateCheck": "boolean",
|
||||||
|
"openSeaEnabled": "boolean",
|
||||||
|
"advancedGasFee": "object",
|
||||||
|
"lostIdentities": "object",
|
||||||
|
"forgottenPassword": false,
|
||||||
|
"ipfsGateway": "dweb.link",
|
||||||
|
"infuraBlocked": "boolean",
|
||||||
|
"ledgerTransportType": "string",
|
||||||
|
"snapRegistryList": "object",
|
||||||
|
"transactionSecurityCheckEnabled": "boolean",
|
||||||
|
"theme": "string",
|
||||||
|
"isLineaMainnetReleased": "boolean",
|
||||||
|
"selectedAddress": "string",
|
||||||
|
"metaMetricsId": "fake-metrics-id",
|
||||||
|
"eventsBeforeMetricsOptIn": "object",
|
||||||
|
"traits": "object",
|
||||||
|
"fragments": "object",
|
||||||
|
"segmentApiCalls": "object",
|
||||||
|
"previousUserTraits": "object",
|
||||||
|
"conversionDate": "number",
|
||||||
|
"currentCurrency": "usd",
|
||||||
|
"pendingCurrentCurrency": "object",
|
||||||
|
"pendingNativeCurrency": "object",
|
||||||
|
"usdConversionRate": "number",
|
||||||
|
"alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true },
|
||||||
|
"unconnectedAccountAlertShownOrigins": "object",
|
||||||
|
"web3ShimUsageOrigins": "object",
|
||||||
|
"seedPhraseBackedUp": true,
|
||||||
|
"onboardingTabs": "object",
|
||||||
|
"incomingTransactions": "object",
|
||||||
|
"incomingTxLastFetchedBlockByChainId": {
|
||||||
|
"0x1": null,
|
||||||
|
"0xe708": null,
|
||||||
|
"0x5": null,
|
||||||
|
"0xaa36a7": null,
|
||||||
|
"0xe704": null
|
||||||
|
},
|
||||||
|
"subjects": "object",
|
||||||
|
"permissionHistory": "object",
|
||||||
|
"permissionActivityLog": "object",
|
||||||
|
"subjectMetadata": "object",
|
||||||
|
"announcements": "object",
|
||||||
|
"gasFeeEstimates": "object",
|
||||||
|
"estimatedGasFeeTimeBounds": "object",
|
||||||
|
"gasEstimateType": "string",
|
||||||
|
"tokenList": "object",
|
||||||
|
"tokensChainsCache": "object",
|
||||||
|
"preventPollingOnNetworkRestart": "boolean",
|
||||||
|
"tokens": "object",
|
||||||
|
"ignoredTokens": "object",
|
||||||
|
"detectedTokens": "object",
|
||||||
|
"allTokens": "object",
|
||||||
|
"allIgnoredTokens": "object",
|
||||||
|
"allDetectedTokens": "object",
|
||||||
|
"smartTransactionsState": "object",
|
||||||
|
"allNftContracts": "object",
|
||||||
|
"allNfts": "object",
|
||||||
|
"ignoredNfts": "object",
|
||||||
|
"accounts": "object",
|
||||||
|
"currentNetworkTxList": "object",
|
||||||
|
"unapprovedDecryptMsgs": "object",
|
||||||
|
"unapprovedDecryptMsgCount": 0,
|
||||||
|
"unapprovedEncryptionPublicKeyMsgs": "object",
|
||||||
|
"unapprovedEncryptionPublicKeyMsgCount": 0,
|
||||||
|
"unapprovedMsgs": "object",
|
||||||
|
"unapprovedPersonalMsgs": "object",
|
||||||
|
"unapprovedTypedMessages": "object",
|
||||||
|
"unapprovedMsgCount": 0,
|
||||||
|
"unapprovedPersonalMsgCount": 0,
|
||||||
|
"unapprovedTypedMessagesCount": 0,
|
||||||
|
"swapsState": "object",
|
||||||
|
"ensResolutionsByAddress": "object",
|
||||||
|
"pendingApprovals": "object",
|
||||||
|
"pendingApprovalCount": "number",
|
||||||
|
"approvalFlows": "object"
|
||||||
|
},
|
||||||
|
"send": "object",
|
||||||
|
"swaps": "object",
|
||||||
|
"unconnectedAccount": { "state": "CLOSED" }
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"AlertController": {
|
||||||
|
"alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true },
|
||||||
|
"unconnectedAccountAlertShownOrigins": "object",
|
||||||
|
"web3ShimUsageOrigins": "object"
|
||||||
|
},
|
||||||
|
"AnnouncementController": "object",
|
||||||
|
"AppStateController": {
|
||||||
|
"browserEnvironment": "object",
|
||||||
|
"nftsDropdownState": "object",
|
||||||
|
"connectedStatusPopoverHasBeenShown": true,
|
||||||
|
"termsOfUseLastAgreed": "number",
|
||||||
|
"defaultHomeActiveTabName": null,
|
||||||
|
"fullScreenGasPollTokens": "object",
|
||||||
|
"notificationGasPollTokens": "object",
|
||||||
|
"popupGasPollTokens": "object",
|
||||||
|
"qrHardware": "object",
|
||||||
|
"recoveryPhraseReminderHasBeenShown": "boolean",
|
||||||
|
"recoveryPhraseReminderLastShown": "number",
|
||||||
|
"showTestnetMessageInDropdown": "boolean",
|
||||||
|
"trezorModel": "object",
|
||||||
|
"usedNetworks": "object",
|
||||||
|
"snapsInstallPrivacyWarningShown": "boolean"
|
||||||
|
},
|
||||||
|
"CachedBalancesController": "object",
|
||||||
|
"CurrencyController": {
|
||||||
|
"conversionDate": 1665507600,
|
||||||
|
"conversionRate": 1300,
|
||||||
|
"currentCurrency": "usd",
|
||||||
|
"nativeCurrency": "ETH",
|
||||||
|
"usdConversionRate": "number"
|
||||||
|
},
|
||||||
|
"GasFeeController": "object",
|
||||||
|
"IncomingTransactionsController": {
|
||||||
|
"incomingTransactions": "object",
|
||||||
|
"incomingTxLastFetchedBlockByChainId": {
|
||||||
|
"0x1": null,
|
||||||
|
"0xe708": null,
|
||||||
|
"0x5": null,
|
||||||
|
"0xaa36a7": null,
|
||||||
|
"0xe704": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"KeyringController": { "vault": "string" },
|
||||||
|
"MetaMetricsController": {
|
||||||
|
"eventsBeforeMetricsOptIn": "object",
|
||||||
|
"fragments": "object",
|
||||||
|
"metaMetricsId": "fake-metrics-id",
|
||||||
|
"participateInMetaMetrics": true,
|
||||||
|
"traits": "object"
|
||||||
|
},
|
||||||
|
"NetworkController": {
|
||||||
|
"networkId": "1337",
|
||||||
|
"networkStatus": "available",
|
||||||
|
"providerConfig": {
|
||||||
|
"chainId": "string",
|
||||||
|
"nickname": "Localhost 8545",
|
||||||
|
"rpcPrefs": "object",
|
||||||
|
"rpcUrl": "string",
|
||||||
|
"ticker": "ETH",
|
||||||
|
"type": "rpc",
|
||||||
|
"id": "string"
|
||||||
|
},
|
||||||
|
"networkConfigurations": "object"
|
||||||
|
},
|
||||||
|
"OnboardingController": {
|
||||||
|
"completedOnboarding": true,
|
||||||
|
"firstTimeFlowType": "import",
|
||||||
|
"onboardingTabs": "object",
|
||||||
|
"seedPhraseBackedUp": true
|
||||||
|
},
|
||||||
|
"PermissionController": "object",
|
||||||
|
"PreferencesController": {
|
||||||
|
"advancedGasFee": "object",
|
||||||
|
"currentLocale": "en",
|
||||||
|
"dismissSeedBackUpReminder": "boolean",
|
||||||
|
"featureFlags": { "showIncomingTransactions": true },
|
||||||
|
"forgottenPassword": false,
|
||||||
|
"identities": "object",
|
||||||
|
"infuraBlocked": "boolean",
|
||||||
|
"ipfsGateway": "dweb.link",
|
||||||
|
"knownMethodData": "object",
|
||||||
|
"ledgerTransportType": "string",
|
||||||
|
"lostIdentities": "object",
|
||||||
|
"openSeaEnabled": "boolean",
|
||||||
|
"preferences": {
|
||||||
|
"hideZeroBalanceTokens": false,
|
||||||
|
"showFiatInTestnets": false,
|
||||||
|
"showTestNetworks": false,
|
||||||
|
"useNativeCurrencyAsPrimaryCurrency": true
|
||||||
|
},
|
||||||
|
"selectedAddress": "string",
|
||||||
|
"theme": "string",
|
||||||
|
"useBlockie": false,
|
||||||
|
"useNftDetection": "boolean",
|
||||||
|
"useNonceField": false,
|
||||||
|
"usePhishDetect": true,
|
||||||
|
"useTokenDetection": "boolean",
|
||||||
|
"useCurrencyRateCheck": "boolean",
|
||||||
|
"useMultiAccountBalanceChecker": "boolean"
|
||||||
|
},
|
||||||
|
"SmartTransactionsController": "object",
|
||||||
|
"SubjectMetadataController": "object",
|
||||||
|
"TokensController": "object",
|
||||||
|
"TransactionController": "object",
|
||||||
|
"config": "object",
|
||||||
|
"firstTimeInfo": "object"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"AlertController": {
|
||||||
|
"alertEnabledness": { "unconnectedAccount": true, "web3ShimUsage": true },
|
||||||
|
"unconnectedAccountAlertShownOrigins": "object",
|
||||||
|
"web3ShimUsageOrigins": "object"
|
||||||
|
},
|
||||||
|
"AnnouncementController": "object",
|
||||||
|
"AppStateController": {
|
||||||
|
"browserEnvironment": "object",
|
||||||
|
"nftsDropdownState": "object",
|
||||||
|
"connectedStatusPopoverHasBeenShown": true,
|
||||||
|
"termsOfUseLastAgreed": "number",
|
||||||
|
"defaultHomeActiveTabName": null,
|
||||||
|
"fullScreenGasPollTokens": "object",
|
||||||
|
"notificationGasPollTokens": "object",
|
||||||
|
"popupGasPollTokens": "object",
|
||||||
|
"qrHardware": "object",
|
||||||
|
"recoveryPhraseReminderHasBeenShown": "boolean",
|
||||||
|
"recoveryPhraseReminderLastShown": "number",
|
||||||
|
"showTestnetMessageInDropdown": "boolean",
|
||||||
|
"trezorModel": "object",
|
||||||
|
"usedNetworks": "object",
|
||||||
|
"snapsInstallPrivacyWarningShown": "boolean"
|
||||||
|
},
|
||||||
|
"CachedBalancesController": "object",
|
||||||
|
"CurrencyController": {
|
||||||
|
"conversionDate": 1665507600,
|
||||||
|
"conversionRate": 1300,
|
||||||
|
"currentCurrency": "usd",
|
||||||
|
"nativeCurrency": "ETH",
|
||||||
|
"usdConversionRate": "number"
|
||||||
|
},
|
||||||
|
"GasFeeController": "object",
|
||||||
|
"IncomingTransactionsController": {
|
||||||
|
"incomingTransactions": "object",
|
||||||
|
"incomingTxLastFetchedBlockByChainId": {
|
||||||
|
"0x1": null,
|
||||||
|
"0xe708": null,
|
||||||
|
"0x5": null,
|
||||||
|
"0xaa36a7": null,
|
||||||
|
"0xe704": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"KeyringController": { "vault": "string" },
|
||||||
|
"MetaMetricsController": {
|
||||||
|
"eventsBeforeMetricsOptIn": "object",
|
||||||
|
"fragments": "object",
|
||||||
|
"metaMetricsId": "fake-metrics-id",
|
||||||
|
"participateInMetaMetrics": true,
|
||||||
|
"traits": "object"
|
||||||
|
},
|
||||||
|
"NetworkController": {
|
||||||
|
"networkId": "1337",
|
||||||
|
"networkStatus": "available",
|
||||||
|
"providerConfig": {
|
||||||
|
"chainId": "string",
|
||||||
|
"nickname": "Localhost 8545",
|
||||||
|
"rpcPrefs": "object",
|
||||||
|
"rpcUrl": "string",
|
||||||
|
"ticker": "ETH",
|
||||||
|
"type": "rpc",
|
||||||
|
"id": "string"
|
||||||
|
},
|
||||||
|
"networkConfigurations": "object"
|
||||||
|
},
|
||||||
|
"OnboardingController": {
|
||||||
|
"completedOnboarding": true,
|
||||||
|
"firstTimeFlowType": "import",
|
||||||
|
"onboardingTabs": "object",
|
||||||
|
"seedPhraseBackedUp": true
|
||||||
|
},
|
||||||
|
"PermissionController": "object",
|
||||||
|
"PreferencesController": {
|
||||||
|
"advancedGasFee": "object",
|
||||||
|
"currentLocale": "en",
|
||||||
|
"dismissSeedBackUpReminder": "boolean",
|
||||||
|
"featureFlags": { "showIncomingTransactions": true },
|
||||||
|
"forgottenPassword": false,
|
||||||
|
"identities": "object",
|
||||||
|
"infuraBlocked": "boolean",
|
||||||
|
"ipfsGateway": "dweb.link",
|
||||||
|
"knownMethodData": "object",
|
||||||
|
"ledgerTransportType": "string",
|
||||||
|
"lostIdentities": "object",
|
||||||
|
"openSeaEnabled": "boolean",
|
||||||
|
"preferences": {
|
||||||
|
"hideZeroBalanceTokens": false,
|
||||||
|
"showFiatInTestnets": false,
|
||||||
|
"showTestNetworks": false,
|
||||||
|
"useNativeCurrencyAsPrimaryCurrency": true
|
||||||
|
},
|
||||||
|
"selectedAddress": "string",
|
||||||
|
"theme": "string",
|
||||||
|
"useBlockie": false,
|
||||||
|
"useNftDetection": "boolean",
|
||||||
|
"useNonceField": false,
|
||||||
|
"usePhishDetect": true,
|
||||||
|
"useTokenDetection": "boolean",
|
||||||
|
"useCurrencyRateCheck": "boolean",
|
||||||
|
"useMultiAccountBalanceChecker": "boolean"
|
||||||
|
},
|
||||||
|
"SmartTransactionsController": "object",
|
||||||
|
"SubjectMetadataController": "object",
|
||||||
|
"TokensController": "object",
|
||||||
|
"TransactionController": "object",
|
||||||
|
"config": "object",
|
||||||
|
"firstTimeInfo": "object"
|
||||||
|
},
|
||||||
|
"meta": { "version": 74 }
|
||||||
|
}
|
4
types/global.d.ts
vendored
4
types/global.d.ts
vendored
@ -1,6 +1,7 @@
|
|||||||
// In order for variables to be considered on the global scope they must be
|
// In order for variables to be considered on the global scope they must be
|
||||||
// declared using var and not const or let, which is why this rule is disabled
|
// declared using var and not const or let, which is why this rule is disabled
|
||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
|
import * as Sentry from '@sentry/browser';
|
||||||
|
|
||||||
declare class Platform {
|
declare class Platform {
|
||||||
openTab: (opts: { url: string }) => void;
|
openTab: (opts: { url: string }) => void;
|
||||||
@ -11,6 +12,9 @@ declare class Platform {
|
|||||||
export declare global {
|
export declare global {
|
||||||
var platform: Platform;
|
var platform: Platform;
|
||||||
|
|
||||||
|
// Sentry is undefined in dev, so use optional chaining
|
||||||
|
var sentry: Sentry | undefined;
|
||||||
|
|
||||||
namespace jest {
|
namespace jest {
|
||||||
interface Matchers<R> {
|
interface Matchers<R> {
|
||||||
toBeFulfilled(): Promise<R>;
|
toBeFulfilled(): Promise<R>;
|
||||||
|
60
ui/index.js
60
ui/index.js
@ -8,7 +8,7 @@ import browser from 'webextension-polyfill';
|
|||||||
import { getEnvironmentType } from '../app/scripts/lib/util';
|
import { getEnvironmentType } from '../app/scripts/lib/util';
|
||||||
import { AlertTypes } from '../shared/constants/alerts';
|
import { AlertTypes } from '../shared/constants/alerts';
|
||||||
import { maskObject } from '../shared/modules/object.utils';
|
import { maskObject } from '../shared/modules/object.utils';
|
||||||
import { SENTRY_STATE } from '../app/scripts/lib/setupSentry';
|
import { SENTRY_UI_STATE } from '../app/scripts/lib/setupSentry';
|
||||||
import { ENVIRONMENT_TYPE_POPUP } from '../shared/constants/app';
|
import { ENVIRONMENT_TYPE_POPUP } from '../shared/constants/app';
|
||||||
import switchDirection from '../shared/lib/switch-direction';
|
import switchDirection from '../shared/lib/switch-direction';
|
||||||
import { setupLocale } from '../shared/lib/error-utils';
|
import { setupLocale } from '../shared/lib/error-utils';
|
||||||
@ -27,7 +27,7 @@ import Root from './pages';
|
|||||||
import txHelper from './helpers/utils/tx-helper';
|
import txHelper from './helpers/utils/tx-helper';
|
||||||
import { _setBackgroundConnection } from './store/action-queue';
|
import { _setBackgroundConnection } from './store/action-queue';
|
||||||
|
|
||||||
log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn');
|
log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn', false);
|
||||||
|
|
||||||
let reduxStore;
|
let reduxStore;
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export default function launchMetamaskUi(opts, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
startApp(metamaskState, backgroundConnection, opts).then((store) => {
|
startApp(metamaskState, backgroundConnection, opts).then((store) => {
|
||||||
setupDebuggingHelpers(store);
|
setupStateHooks(store);
|
||||||
cb(
|
cb(
|
||||||
null,
|
null,
|
||||||
store,
|
store,
|
||||||
@ -191,16 +191,39 @@ async function startApp(metamaskState, backgroundConnection, opts) {
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupDebuggingHelpers(store) {
|
/**
|
||||||
/**
|
* Setup functions on `window.stateHooks`. Some of these support
|
||||||
* The following stateHook is a method intended to throw an error, used in
|
* application features, and some are just for debugging or testing.
|
||||||
* our E2E test to ensure that errors are attempted to be sent to sentry.
|
*
|
||||||
*/
|
* @param {object} store - The Redux store.
|
||||||
window.stateHooks.throwTestError = async function () {
|
*/
|
||||||
const error = new Error('Test Error');
|
function setupStateHooks(store) {
|
||||||
error.name = 'TestError';
|
if (process.env.METAMASK_DEBUG || process.env.IN_TEST) {
|
||||||
throw error;
|
/**
|
||||||
};
|
* The following stateHook is a method intended to throw an error, used in
|
||||||
|
* our E2E test to ensure that errors are attempted to be sent to sentry.
|
||||||
|
*
|
||||||
|
* @param {string} [msg] - The error message to throw, defaults to 'Test Error'
|
||||||
|
*/
|
||||||
|
window.stateHooks.throwTestError = async function (msg = 'Test Error') {
|
||||||
|
const error = new Error(msg);
|
||||||
|
error.name = 'TestError';
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* The following stateHook is a method intended to throw an error in the
|
||||||
|
* background, used in our E2E test to ensure that errors are attempted to be
|
||||||
|
* sent to sentry.
|
||||||
|
*
|
||||||
|
* @param {string} [msg] - The error message to throw, defaults to 'Test Error'
|
||||||
|
*/
|
||||||
|
window.stateHooks.throwTestBackgroundError = async function (
|
||||||
|
msg = 'Test Error',
|
||||||
|
) {
|
||||||
|
store.dispatch(actions.throwTestBackgroundError(msg));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
window.stateHooks.getCleanAppState = async function () {
|
window.stateHooks.getCleanAppState = async function () {
|
||||||
const state = clone(store.getState());
|
const state = clone(store.getState());
|
||||||
state.version = global.platform.getVersion();
|
state.version = global.platform.getVersion();
|
||||||
@ -210,14 +233,9 @@ function setupDebuggingHelpers(store) {
|
|||||||
});
|
});
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
window.stateHooks.getSentryState = function () {
|
window.stateHooks.getSentryAppState = function () {
|
||||||
const fullState = store.getState();
|
const reduxState = store.getState();
|
||||||
const debugState = maskObject(fullState, SENTRY_STATE);
|
return maskObject(reduxState, SENTRY_UI_STATE);
|
||||||
return {
|
|
||||||
browser: window.navigator.userAgent,
|
|
||||||
store: debugState,
|
|
||||||
version: global.platform.getVersion(),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4427,6 +4427,17 @@ export async function getCurrentNetworkEIP1559Compatibility(): Promise<
|
|||||||
return networkEIP1559Compatibility;
|
return networkEIP1559Compatibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw an error in the background for testing purposes.
|
||||||
|
*
|
||||||
|
* @param message - The error message.
|
||||||
|
* @deprecated This is only mean to facilitiate E2E testing. We should not use
|
||||||
|
* this for handling errors.
|
||||||
|
*/
|
||||||
|
export async function throwTestBackgroundError(message: string): Promise<void> {
|
||||||
|
await submitRequestToBackground('throwTestError', [message]);
|
||||||
|
}
|
||||||
|
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||||
/**
|
/**
|
||||||
* Set status of popover warning for the first snap installation.
|
* Set status of popover warning for the first snap installation.
|
||||||
|
25
yarn.lock
25
yarn.lock
@ -1891,6 +1891,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@endo/env-options@npm:^0.1.3":
|
||||||
|
version: 0.1.3
|
||||||
|
resolution: "@endo/env-options@npm:0.1.3"
|
||||||
|
checksum: da8c66865d4d30b0053a00960657dc36f022975a888f0dd6a2f6bb37b9fe731f45a02a2cf263d93b1a40fcb37b25f8ba7076cb8af9e93fd95f496365d9382930
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@ensdomains/address-encoder@npm:^0.1.7":
|
"@ensdomains/address-encoder@npm:^0.1.7":
|
||||||
version: 0.1.9
|
version: 0.1.9
|
||||||
resolution: "@ensdomains/address-encoder@npm:0.1.9"
|
resolution: "@ensdomains/address-encoder@npm:0.1.9"
|
||||||
@ -24935,7 +24942,7 @@ __metadata:
|
|||||||
selenium-webdriver: ^4.9.0
|
selenium-webdriver: ^4.9.0
|
||||||
semver: ^7.3.5
|
semver: ^7.3.5
|
||||||
serve-handler: ^6.1.2
|
serve-handler: ^6.1.2
|
||||||
ses: ^0.18.4
|
ses: ^0.18.7
|
||||||
single-call-balance-checker-abi: ^1.0.0
|
single-call-balance-checker-abi: ^1.0.0
|
||||||
sinon: ^9.0.0
|
sinon: ^9.0.0
|
||||||
source-map: ^0.7.2
|
source-map: ^0.7.2
|
||||||
@ -28697,8 +28704,8 @@ __metadata:
|
|||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"protobufjs@npm:^6.11.3":
|
"protobufjs@npm:^6.11.3":
|
||||||
version: 6.11.3
|
version: 6.11.4
|
||||||
resolution: "protobufjs@npm:6.11.3"
|
resolution: "protobufjs@npm:6.11.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@protobufjs/aspromise": ^1.1.2
|
"@protobufjs/aspromise": ^1.1.2
|
||||||
"@protobufjs/base64": ^1.1.2
|
"@protobufjs/base64": ^1.1.2
|
||||||
@ -28716,7 +28723,7 @@ __metadata:
|
|||||||
bin:
|
bin:
|
||||||
pbjs: bin/pbjs
|
pbjs: bin/pbjs
|
||||||
pbts: bin/pbts
|
pbts: bin/pbts
|
||||||
checksum: 4a6ce1964167e4c45c53fd8a312d7646415c777dd31b4ba346719947b88e61654912326101f927da387d6b6473ab52a7ea4f54d6f15d63b31130ce28e2e15070
|
checksum: 6b7fd7540d74350d65c38f69f398c9995ae019da070e79d9cd464a458c6d19b40b07c9a026be4e10704c824a344b603307745863310c50026ebd661ce4da0663
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -31499,10 +31506,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"ses@npm:^0.18.1, ses@npm:^0.18.4":
|
"ses@npm:^0.18.1, ses@npm:^0.18.7":
|
||||||
version: 0.18.4
|
version: 0.18.7
|
||||||
resolution: "ses@npm:0.18.4"
|
resolution: "ses@npm:0.18.7"
|
||||||
checksum: 9afd6edcf390a693926ef728ebb5a435994bbb0f915009ad524c6588cf62e2f66f6d4b4b2694f093b2af2e92c003947ad55404750d756ba75ce70c8636a7ba02
|
dependencies:
|
||||||
|
"@endo/env-options": ^0.1.3
|
||||||
|
checksum: 75ac014771d9bc1f747193c6d0f9e7d2d7700a10311ba8d805d9bc78d4c20d4ef40537f0535b1ea6abf06babf67e70f8bd37b2ad68ad54992a0c5ce842181c87
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user