Skip to content

Commit

Permalink
fix: Delete invalid SelectedNetworkController state
Browse files Browse the repository at this point in the history
The `SelectedNetworkController` state is cleared if any invalid
`networkConfigurationId`s are found in state. We are seeing reports of
this happening in production in v12.0.1.

The suspected cause is `NetworkController` state corruption. We
resolved a few cases of this in v12.0.1, but for users that were
affected by this, the invalid IDs may have propogated to the
`SelectedNetworkController` state already. That is what this migration
intends to fix.

Fixes #26309
  • Loading branch information
Gudahtt committed Aug 14, 2024
1 parent c5c13d5 commit 70795ce
Show file tree
Hide file tree
Showing 2 changed files with 353 additions and 0 deletions.
247 changes: 247 additions & 0 deletions app/scripts/migrations/120.5.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { cloneDeep } from 'lodash';
import { migrate, version } from './120.5';

const oldVersion = 120.4;

describe('migration #120.5', () => {
afterEach(() => {
jest.resetAllMocks();
});

it('updates the version metadata', async () => {
const oldStorage = {
meta: { version: oldVersion },
data: {},
};

const newStorage = await migrate(cloneDeep(oldStorage));

expect(newStorage.meta).toStrictEqual({ version });
});

it('does nothing if SelectedNetworkController state is not set', async () => {
const oldState = {
NetworkController: {
networkConfigurations: {
123: {},
},
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual(oldState);
});

it('deletes the SelectedNetworkController state if it is corrupted', async () => {
const oldState = {
NetworkController: {
networkConfigurations: {
123: {},
},
},
SelectedNetworkController: 'invalid',
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual({
NetworkController: {
networkConfigurations: {
123: {},
},
},
});
});

it('deletes the SelectedNetworkController state if it is missing the domains state', async () => {
const oldState = {
NetworkController: {
networkConfigurations: {
123: {},
},
},
SelectedNetworkController: {
somethingElse: {},
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual({
NetworkController: {
networkConfigurations: {
123: {},
},
},
});
});

it('deletes the SelectedNetworkController state if the domains state is corrupted', async () => {
const oldState = {
NetworkController: {
networkConfigurations: {
123: {},
},
},
SelectedNetworkController: {
domains: 'invalid',
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual({
NetworkController: {
networkConfigurations: {
123: {},
},
},
});
});

it('deletes the SelectedNetworkController state if NetworkController state is missing', async () => {
const oldState = {
SelectedNetworkController: {
domains: {},
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual({});
});

it('deletes the SelectedNetworkController state if NetworkController state is corrupted', async () => {
const oldState = {
NetworkController: 'invalid',
SelectedNetworkController: {
domains: {},
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual({
NetworkController: 'invalid',
});
});

it('deletes the SelectedNetworkController state if NetworkController has no networkConfigurations', async () => {
const oldState = {
NetworkController: {},
SelectedNetworkController: {
domains: {},
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual({
NetworkController: {},
});
});

it('deletes the SelectedNetworkController state if NetworkController networkConfigurations state is corrupted', async () => {
const oldState = {
NetworkController: { networkConfigurations: 'invalid' },
SelectedNetworkController: {
domains: {},
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual({
NetworkController: { networkConfigurations: 'invalid' },
});
});

it('does nothing if SelectedNetworkController domains state is empty', async () => {
const oldState = {
NetworkController: { networkConfigurations: {} },
SelectedNetworkController: {
domains: {},
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual(oldState);
});

it('does nothing if SelectedNetworkController domains state is valid', async () => {
const oldState = {
NetworkController: {
networkConfigurations: {
123: {},
},
},
SelectedNetworkController: {
domains: {},
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual(oldState);
});

it('deletes the SelectedNetworkController state if an invalid networkConfigurationId is found', async () => {
const oldState = {
NetworkController: {
networkConfigurations: {
123: {},
},
},
SelectedNetworkController: {
domains: {
'domain.test': '456',
},
},
};

const transformedState = await migrate({
meta: { version: oldVersion },
data: cloneDeep(oldState),
});

expect(transformedState.data).toEqual({
NetworkController: {
networkConfigurations: {
123: {},
},
},
});
});
});
106 changes: 106 additions & 0 deletions app/scripts/migrations/120.5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { hasProperty, isObject } from '@metamask/utils';
import { cloneDeep } from 'lodash';

type VersionedData = {
meta: { version: number };
data: Record<string, unknown>;
};

export const version = 120.5;

/**
* This migration removes invalid network configuration IDs from the SelectedNetworkController.
*
* @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: VersionedData,
): Promise<VersionedData> {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
transformState(versionedData.data);
return versionedData;
}

/**
* Remove invalid network configuration IDs from the SelectedNetworkController.
*
* @param state - The persisted MetaMask state, keyed by controller.
*/
function transformState(state: Record<string, unknown>): void {
if (!hasProperty(state, 'SelectedNetworkController')) {
return;
}
if (!isObject(state.SelectedNetworkController)) {
console.error(
`Migration ${version}: Invalid SelectedNetworkController state of type '${typeof state.SelectedNetworkController}'`,
);
delete state.SelectedNetworkController;
return;
} else if (!hasProperty(state.SelectedNetworkController, 'domains')) {
console.error(
`Migration ${version}: Missing SelectedNetworkController domains state`,
);
delete state.SelectedNetworkController;
return;
} else if (!isObject(state.SelectedNetworkController.domains)) {
console.error(
`Migration ${version}: Invalid SelectedNetworkController domains state of type '${typeof state
.SelectedNetworkController.domains}'`,
);
delete state.SelectedNetworkController;
return;
}

if (!hasProperty(state, 'NetworkController')) {
delete state.SelectedNetworkController;
return;
} else if (!isObject(state.NetworkController)) {
console.error(
new Error(
`Migration ${version}: Invalid NetworkController state of type '${typeof state.NetworkController}'`,
),
);
delete state.SelectedNetworkController;
return;
} else if (!hasProperty(state.NetworkController, 'networkConfigurations')) {
delete state.SelectedNetworkController;
return;
} else if (!isObject(state.NetworkController.networkConfigurations)) {
console.error(
new Error(
`Migration ${version}: Invalid NetworkController networkConfigurations state of type '${typeof state.NetworkController}'`,
),
);
delete state.SelectedNetworkController;
return;
}

const validNetworkConfigurationIds = Object.keys(
state.NetworkController.networkConfigurations,
);
const domainMappedNetworkConfigurationIds = Object.values(
state.SelectedNetworkController.domains,
);

for (const configurationId of domainMappedNetworkConfigurationIds) {
if (
typeof configurationId !== 'string' ||
!validNetworkConfigurationIds.includes(configurationId)
) {
console.error(
new Error(
`Migration ${version}: Invalid networkConfigurationId found in SelectedNetworkController state: '${configurationId}'`,
),
);
delete state.SelectedNetworkController;
return;
}
}
}

0 comments on commit 70795ce

Please sign in to comment.