-
Notifications
You must be signed in to change notification settings - Fork 5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Delete invalid
SelectedNetworkController
state
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
Showing
2 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: {}, | ||
}, | ||
}, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |