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

Capture exception with sentry when invariant conditions are met in migrations (#20427)

* capture exception for sentry when invariant conditions are met in migration 82

* Code cleanup

* Capture exceptions in invariant conditions for migrations 83,84,85,86,89,91,93,94

* Update app/scripts/migrations/082.test.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

* Code cleanup

* Fix SentryObject type declaration

* Stop throwing error if preferences controller is undefined

* Refactor 084 and 086 to remove double negative

* Capture exceptions for invariant states in in migrations 87,88,90 and 92

* lint fix

* log warning in migration 82 when preferences controller is undefined

---------

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
Dan J Miller 2023-08-16 16:56:20 -02:30 committed by Dan Miller
parent 1ad47c660b
commit 915bf2ae88
23 changed files with 1153 additions and 17 deletions

View File

@ -1,6 +1,12 @@
import { v4 } from 'uuid';
import { migrate, version } from './082';
const sentryCaptureExceptionMock = jest.fn();
global.sentry = {
captureException: sentryCaptureExceptionMock,
};
jest.mock('uuid', () => {
const actual = jest.requireActual('uuid');
@ -472,10 +478,72 @@ describe('migration #82', () => {
},
};
const newStorage = await migrate(oldStorage);
expect(sentryCaptureExceptionMock).toHaveBeenCalled();
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 = {
meta: {
version: 81,
@ -556,9 +624,60 @@ describe('migration #82', () => {
},
};
const newStorage = await migrate(oldStorage);
expect(sentryCaptureExceptionMock).not.toHaveBeenCalled();
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 () => {
const oldStorage = {
meta: {
@ -595,4 +714,61 @@ describe('migration #82', () => {
const newStorage = await migrate(oldStorage);
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`),
);
});
});

View File

@ -1,6 +1,7 @@
import { cloneDeep } from 'lodash';
import { hasProperty, isObject } from '@metamask/utils';
import { v4 } from 'uuid';
import log from 'loglevel';
export const version = 82;
@ -25,14 +26,56 @@ export async function migrate(originalVersionedData: {
}
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 (
!hasProperty(state, 'PreferencesController') ||
!isObject(state.PreferencesController) ||
!isObject(state.NetworkController) ||
!hasProperty(state.PreferencesController, 'frequentRpcListDetail') ||
!Array.isArray(state.PreferencesController.frequentRpcListDetail) ||
!state.PreferencesController.frequentRpcListDetail.every(isObject)
!hasProperty(state, 'NetworkController') ||
!isObject(state.NetworkController)
) {
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;
}
const { PreferencesController, NetworkController } = state;

View File

@ -1,6 +1,12 @@
import { v4 } from 'uuid';
import { migrate, version } from './083';
const sentryCaptureExceptionMock = jest.fn();
global.sentry = {
captureException: sentryCaptureExceptionMock,
};
jest.mock('uuid', () => {
const actual = jest.requireActual('uuid');
@ -165,6 +171,24 @@ describe('migration #83', () => {
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 () => {
const oldStorage = {
meta: {
@ -190,6 +214,25 @@ describe('migration #83', () => {
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 () => {
const oldStorage = {
meta: {
@ -221,6 +264,28 @@ describe('migration #83', () => {
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 () => {
const oldStorage = {
meta: {

View File

@ -25,11 +25,21 @@ export async function migrate(originalVersionedData: {
function transformState(state: Record<string, unknown>) {
if (!isObject(state.NetworkController)) {
global.sentry?.captureException?.(
new Error(
`typeof state.NetworkController is ${typeof state.NetworkController}`,
),
);
return state;
}
const { NetworkController } = state;
if (!isObject(NetworkController.networkConfigurations)) {
global.sentry?.captureException?.(
new Error(
`typeof NetworkController.networkConfigurations is ${typeof NetworkController.networkConfigurations}`,
),
);
return state;
}

View File

@ -1,6 +1,16 @@
import { migrate } from './084';
const sentryCaptureExceptionMock = jest.fn();
global.sentry = {
captureException: sentryCaptureExceptionMock,
};
describe('migration #84', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('updates the version metadata', async () => {
const originalVersionedData = buildOriginalVersionedData({
meta: {
@ -27,6 +37,21 @@ describe('migration #84', () => {
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']];
for (const invalidState of nonObjects) {
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);
});
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 () => {
@ -56,6 +96,38 @@ describe('migration #84', () => {
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 () => {
const originalVersionedData = buildOriginalVersionedData({
data: {

View File

@ -25,9 +25,26 @@ export async function migrate(originalVersionedData: {
function transformState(state: Record<string, unknown>) {
if (
!hasProperty(state, 'NetworkController') ||
!isObject(state.NetworkController) ||
!hasProperty(state.NetworkController, 'network')
!isObject(state.NetworkController)
) {
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;
}

View File

@ -1,5 +1,11 @@
import { migrate, version } from './085';
const sentryCaptureExceptionMock = jest.fn();
global.sentry = {
captureException: sentryCaptureExceptionMock,
};
jest.mock('uuid', () => {
const actual = jest.requireActual('uuid');
@ -10,6 +16,10 @@ jest.mock('uuid', () => {
});
describe('migration #85', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
@ -39,6 +49,25 @@ describe('migration #85', () => {
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 () => {
const oldData = {
other: 'data',

View File

@ -24,6 +24,11 @@ export async function migrate(originalVersionedData: {
function transformState(state: Record<string, unknown>) {
if (!isObject(state.NetworkController)) {
global.sentry?.captureException?.(
new Error(
`typeof state.NetworkController is ${typeof state.NetworkController}`,
),
);
return state;
}

View File

@ -1,5 +1,11 @@
import { migrate, version } from './086';
const sentryCaptureExceptionMock = jest.fn();
global.sentry = {
captureException: sentryCaptureExceptionMock,
};
jest.mock('uuid', () => {
const actual = jest.requireActual('uuid');
@ -10,6 +16,10 @@ jest.mock('uuid', () => {
});
describe('migration #86', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
@ -39,6 +49,25 @@ describe('migration #86', () => {
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 () => {
const oldData = {
other: 'data',
@ -59,6 +88,52 @@ describe('migration #86', () => {
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 () => {
const oldData = {
other: 'data',

View File

@ -37,5 +37,24 @@ function transformState(state: Record<string, unknown>) {
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;
}

View File

@ -1,6 +1,16 @@
import { migrate, version } from './087';
const sentryCaptureExceptionMock = jest.fn();
global.sentry = {
captureException: sentryCaptureExceptionMock,
};
describe('migration #87', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
@ -53,6 +63,65 @@ describe('migration #87', () => {
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 () => {
const oldData = {
other: 'data',

View File

@ -24,6 +24,11 @@ export async function migrate(originalVersionedData: {
function transformState(state: Record<string, unknown>) {
if (!isObject(state.TokensController)) {
global.sentry?.captureException?.(
new Error(
`typeof state.TokensController is ${typeof state.TokensController}`,
),
);
return state;
}

View File

@ -1,6 +1,16 @@
import { migrate } from './088';
const sentryCaptureExceptionMock = jest.fn();
global.sentry = {
captureException: sentryCaptureExceptionMock,
};
describe('migration #88', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('updates the version metadata', async () => {
const oldStorage = {
meta: { version: 87 },
@ -26,6 +36,24 @@ describe('migration #88', () => {
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 () => {
const oldData = {
NftController: {
@ -58,6 +86,26 @@ describe('migration #88', () => {
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 () => {
const oldData = {
NftController: {
@ -324,6 +372,26 @@ describe('migration #88', () => {
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 () => {
const oldData = {
NftController: {
@ -656,6 +724,91 @@ describe('migration #88', () => {
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 () => {
const oldData = {
TokenListController: {
@ -688,6 +841,25 @@ describe('migration #88', () => {
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 () => {
const oldStorage = {
meta: { version: 87 },
@ -919,6 +1091,39 @@ describe('migration #88', () => {
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 () => {
const oldData = {
TokensController: {
@ -951,6 +1156,25 @@ describe('migration #88', () => {
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 () => {
const oldStorage = {
meta: { version: 87 },
@ -1163,6 +1387,25 @@ describe('migration #88', () => {
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 () => {
const oldStorage = {
meta: { version: 87 },
@ -1323,6 +1566,25 @@ describe('migration #88', () => {
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 () => {
const oldStorage = {
meta: { version: 87 },

View File

@ -1,6 +1,7 @@
import { hasProperty, Hex, isObject, isStrictHexString } from '@metamask/utils';
import { BN } from 'ethereumjs-util';
import { cloneDeep, mapKeys } from 'lodash';
import log from 'loglevel';
type VersionedData = {
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
@ -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;
} 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 (
@ -124,7 +151,24 @@ function migrateData(state: Record<string, unknown>): void {
tokenListControllerState.tokensChainsCache,
(_, 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 (
@ -150,6 +194,16 @@ function migrateData(state: Record<string, unknown>): void {
allTokens,
(_, 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
@ -169,6 +223,16 @@ function migrateData(state: Record<string, unknown>): void {
allIgnoredTokens,
(_, 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
@ -188,9 +252,25 @@ function migrateData(state: Record<string, unknown>): void {
allDetectedTokens,
(_, 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;
} else {
global.sentry?.captureException?.(
new Error(
`typeof state.TokensController is ${typeof state.TokensController}`,
),
);
}
}

View File

@ -1,5 +1,14 @@
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', () => {
const actual = jest.requireActual('uuid');
@ -10,6 +19,10 @@ jest.mock('uuid', () => {
});
describe('migration #89', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
@ -39,6 +52,25 @@ describe('migration #89', () => {
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 () => {
const oldData = {
other: 'data',
@ -61,6 +93,32 @@ describe('migration #89', () => {
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 () => {
const oldData = {
other: 'data',

View File

@ -66,6 +66,19 @@ function transformState(state: Record<string, unknown>) {
...state,
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;
}

View File

@ -2,7 +2,17 @@ import { migrate, version } from './090';
const PREVIOUS_VERSION = version - 1;
const sentryCaptureExceptionMock = jest.fn();
global.sentry = {
captureException: sentryCaptureExceptionMock,
};
describe('migration #90', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('updates the version metadata', async () => {
const oldStorage = {
meta: {
@ -31,7 +41,7 @@ describe('migration #90', () => {
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 = {
meta: {
version: PREVIOUS_VERSION,
@ -39,9 +49,12 @@ describe('migration #90', () => {
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 () => {

View File

@ -1,5 +1,6 @@
import { cloneDeep } from 'lodash';
import { hasProperty, isObject } from '@metamask/utils';
import log from 'loglevel';
export const version = 90;
@ -23,11 +24,22 @@ export async function migrate(originalVersionedData: {
}
function transformState(state: Record<string, unknown>) {
if (
!hasProperty(state, 'PhishingController') ||
!isObject(state.PhishingController) ||
!hasProperty(state.PhishingController, 'listState')
) {
if (!hasProperty(state, 'PhishingController')) {
log.warn(`typeof state.PhishingController is undefined`);
return state;
}
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;
}

View File

@ -1,6 +1,15 @@
import { cloneDeep } from 'lodash';
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', () => {
const actual = jest.requireActual('uuid');
@ -11,6 +20,10 @@ jest.mock('uuid', () => {
});
describe('migration #91', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
@ -40,6 +53,25 @@ describe('migration #91', () => {
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 () => {
const oldData = {
other: 'data',
@ -60,6 +92,32 @@ describe('migration #91', () => {
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 () => {
const oldData = {
other: 'data',

View File

@ -50,6 +50,19 @@ function transformState(state: Record<string, unknown>) {
...state,
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;
}

View File

@ -3,7 +3,20 @@ import { migrate, version } from './092';
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', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
@ -33,6 +46,22 @@ describe('migration #92', () => {
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 () => {
const oldData = {
other: 'data',

View File

@ -1,5 +1,6 @@
import { cloneDeep } from 'lodash';
import { hasProperty, isObject } from '@metamask/utils';
import log from 'loglevel';
export const version = 92;
@ -30,6 +31,14 @@ function transformState(state: Record<string, unknown>) {
) {
delete state.PhishingController.stalelistLastFetched;
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;
}

4
types/global.d.ts vendored
View File

@ -1,6 +1,7 @@
// 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
/* eslint-disable no-var */
import * as Sentry from '@sentry/browser';
declare class Platform {
openTab: (opts: { url: string }) => void;
@ -11,6 +12,9 @@ declare class Platform {
export declare global {
var platform: Platform;
// Sentry is undefined in dev, so use optional chaining
var sentry: Sentry | undefined;
namespace jest {
interface Matchers<R> {
toBeFulfilled(): Promise<R>;