diff --git a/app/scripts/migrations/077-supplements/077-supplement-for-082.ts b/app/scripts/migrations/077-supplements/077-supplement-for-082.ts new file mode 100644 index 000000000..149b2ab0c --- /dev/null +++ b/app/scripts/migrations/077-supplements/077-supplement-for-082.ts @@ -0,0 +1,25 @@ +import { hasProperty, isObject } from '@metamask/utils'; + +/** + * Deletes frequentRpcListDetail if networkConfigurations exists, on the NetworkController state. + * Further explanation in ./077-supplements.md + * + * @param state - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ + +export default function transformState077For082( + state: Record, +) { + if ( + hasProperty(state, 'PreferencesController') && + isObject(state.PreferencesController) && + hasProperty(state.PreferencesController, 'frequentRpcListDetail') && + isObject(state.NetworkController) && + hasProperty(state.NetworkController, 'networkConfigurations') + ) { + delete state.PreferencesController.frequentRpcListDetail; + } + + return { ...state }; +} diff --git a/app/scripts/migrations/077-supplements/077-supplement-for-084.ts b/app/scripts/migrations/077-supplements/077-supplement-for-084.ts new file mode 100644 index 000000000..397efec01 --- /dev/null +++ b/app/scripts/migrations/077-supplements/077-supplement-for-084.ts @@ -0,0 +1,24 @@ +import { hasProperty, isObject } from '@metamask/utils'; + +/** + * Deletes network if networkId exists, on the NetworkController state. + * Further explanation in ./077-supplements.md + * + * @param state - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ + +export default function transformState077For084( + state: Record, +) { + if ( + hasProperty(state, 'NetworkController') && + isObject(state.NetworkController) && + hasProperty(state.NetworkController, 'network') && + hasProperty(state.NetworkController, 'networkId') + ) { + delete state.NetworkController.network; + } + + return { ...state }; +} diff --git a/app/scripts/migrations/077-supplements/077-supplement-for-086.ts b/app/scripts/migrations/077-supplements/077-supplement-for-086.ts new file mode 100644 index 000000000..bad44820e --- /dev/null +++ b/app/scripts/migrations/077-supplements/077-supplement-for-086.ts @@ -0,0 +1,23 @@ +import { hasProperty, isObject } from '@metamask/utils'; + +/** + * Prior to token detection v2 the data property in tokensChainsCache was an array, + * in v2 we changes that to an object. In this migration we are converting the data as array to object. + * + * @param state - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export default function transformState077For086( + state: Record, +) { + if ( + hasProperty(state, 'NetworkController') && + isObject(state.NetworkController) && + hasProperty(state.NetworkController, 'provider') && + hasProperty(state.NetworkController, 'providerConfig') + ) { + delete state.NetworkController.provider; + } + + return { ...state }; +} diff --git a/app/scripts/migrations/077-supplements/077-supplement-for-088.ts b/app/scripts/migrations/077-supplements/077-supplement-for-088.ts new file mode 100644 index 000000000..4d430865a --- /dev/null +++ b/app/scripts/migrations/077-supplements/077-supplement-for-088.ts @@ -0,0 +1,152 @@ +import { hasProperty, isObject, isStrictHexString } from '@metamask/utils'; + +/** + * Deletes properties of `NftController.allNftContracts`, `NftController.allNfts`, + * `TokenListController.tokensChainsCache`, `TokensController.allTokens`, + * `TokensController.allIgnoredTokens` and `TokensController.allDetectedTokens` if + * their keyed by decimal number chainId and another hexadecimal chainId property + * exists within the same object. + * Further explanation in ./077-supplements.md + * + * @param state - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export default function transformState077For086( + state: Record, +): Record { + if (hasProperty(state, 'NftController') && isObject(state.NftController)) { + const nftControllerState = state.NftController; + + // Migrate NftController.allNftContracts + if ( + hasProperty(nftControllerState, 'allNftContracts') && + isObject(nftControllerState.allNftContracts) + ) { + const { allNftContracts } = nftControllerState; + + if ( + Object.keys(allNftContracts).every((address) => + isObject(allNftContracts[address]), + ) + ) { + Object.keys(allNftContracts).forEach((address) => { + const nftContractsByChainId = allNftContracts[address]; + if ( + isObject(nftContractsByChainId) && + anyKeysAreHex(nftContractsByChainId) + ) { + for (const chainId of Object.keys(nftContractsByChainId)) { + if (!isStrictHexString(chainId)) { + delete nftContractsByChainId[chainId]; + } + } + } + }); + } + } + + // Migrate NftController.allNfts + if ( + hasProperty(nftControllerState, 'allNfts') && + isObject(nftControllerState.allNfts) + ) { + const { allNfts } = nftControllerState; + + if (Object.keys(allNfts).every((address) => isObject(allNfts[address]))) { + Object.keys(allNfts).forEach((address) => { + const nftsByChainId = allNfts[address]; + if (isObject(nftsByChainId) && anyKeysAreHex(nftsByChainId)) { + for (const chainId of Object.keys(nftsByChainId)) { + if (!isStrictHexString(chainId)) { + delete nftsByChainId[chainId]; + } + } + } + }); + } + } + + state.NftController = nftControllerState; + } + + if ( + hasProperty(state, 'TokenListController') && + isObject(state.TokenListController) + ) { + const tokenListControllerState = state.TokenListController; + + // Migrate TokenListController.tokensChainsCache + if ( + hasProperty(tokenListControllerState, 'tokensChainsCache') && + isObject(tokenListControllerState.tokensChainsCache) && + anyKeysAreHex(tokenListControllerState.tokensChainsCache) + ) { + for (const chainId of Object.keys( + tokenListControllerState.tokensChainsCache, + )) { + if (!isStrictHexString(chainId)) { + delete tokenListControllerState.tokensChainsCache[chainId]; + } + } + } + } + + if ( + hasProperty(state, 'TokensController') && + isObject(state.TokensController) + ) { + const tokensControllerState = state.TokensController; + + // Migrate TokensController.allTokens + if ( + hasProperty(tokensControllerState, 'allTokens') && + isObject(tokensControllerState.allTokens) && + anyKeysAreHex(tokensControllerState.allTokens) + ) { + const { allTokens } = tokensControllerState; + + for (const chainId of Object.keys(allTokens)) { + if (!isStrictHexString(chainId)) { + delete tokensControllerState.allTokens[chainId]; + } + } + } + + // Migrate TokensController.allIgnoredTokens + if ( + hasProperty(tokensControllerState, 'allIgnoredTokens') && + isObject(tokensControllerState.allIgnoredTokens) && + anyKeysAreHex(tokensControllerState.allIgnoredTokens) + ) { + const { allIgnoredTokens } = tokensControllerState; + + for (const chainId of Object.keys(allIgnoredTokens)) { + if (!isStrictHexString(chainId)) { + delete tokensControllerState.allIgnoredTokens[chainId]; + } + } + } + + // Migrate TokensController.allDetectedTokens + if ( + hasProperty(tokensControllerState, 'allDetectedTokens') && + isObject(tokensControllerState.allDetectedTokens) && + anyKeysAreHex(tokensControllerState.allDetectedTokens) + ) { + const { allDetectedTokens } = tokensControllerState; + + for (const chainId of Object.keys(allDetectedTokens)) { + if (!isStrictHexString(chainId)) { + delete tokensControllerState.allDetectedTokens[chainId]; + } + } + } + + state.TokensController = tokensControllerState; + } + return state; +} + +function anyKeysAreHex(obj: Record) { + return Object.keys(obj).some((chainId) => isStrictHexString(chainId)); +} diff --git a/app/scripts/migrations/077-supplements/077-supplements.md b/app/scripts/migrations/077-supplements/077-supplements.md new file mode 100644 index 000000000..ca40d9852 --- /dev/null +++ b/app/scripts/migrations/077-supplements/077-supplements.md @@ -0,0 +1,100 @@ +# 077 Supplements + +As of the time this file was first PR'd, we had not yet had to do what was done in this PR, which is to fix an old migration and also supplement it with state transformations +to handle problems that could be introduced by future migrations. + +The document explains the need for these new state transformations and the rationale behind each. It also explains why other state transformations were not included. + +## Background + +As of release 10.34.0, we started having a `No metadata found for 'previousProviderStore'` error thrown from the `deriveStateFromMetadata` function in `BaseControllerV2.js`. +This was occuring when there was data on the NetworkController state for which the NetworkController + BaseController expect metadata, but no metadata exists. In particular, +`previousProviderStore` was on the NetworkController state when it should not have been. + +`previousProviderStore` should not have been on the NetworkController state because of migration 85, which explictly deletes it. + +We discovered that for some users, that migration had failed to run because of an error in an earlier migration: `TypeError#1: MetaMask Migration Error #77: Cannot convert undefined or null to object`. +This error was thrown from this line https://github.com/MetaMask/metamask-extension/commit/8f18e04b97af02e5a8a72e3e4872aac66595d1d8#diff-9e76a7c60c1e37cd949f729222338b23ab743e44938ccf63a4a6dab7d84ed8bcR38 + +So the `data` property of the objects within `TokenListController.tokensChainsCache` could be undefined, and migration 77 didn't handle that case. It could be undefined because of the way the assets controller +code was as of the core controller libraries 14.0.2 release https://github.com/MetaMask/core/blame/19f7bf3b9fd8abe6cef9cb1ac1fe831d9f651ae0/src/assets/TokenListController.ts#L270 (the `safelyExecute` call there +will return undefined if the network call failed) + +For users who were in that situation, where a `TokenListController.tokensChainsCache[chainId].data` property was undefined, some significant problems would occur after updating to v10.24.0, which is the +release where migration 77 was shipped. In particular, migration 77 would fail, and all subsequent migrations would not run. The most plain case of this would be a user who was on v10.23.0 +with `TokenListController.tokensChainsCache[chainId].data === undefined`. Then suppose they didn't update until v10.34.0. None of migrations 77-89 would run. Leaving their state in a form that doesn't match +with assumptions throughout our codebase. Various bugs could be caused, but the sentry error describe above is the worst case, where MetaMask simply could not be opened and users would hit the error screen. + +To correct this situation we had to fix migration 77. Once we do that, all users who were in this situation (and then upgraded to the version which included the fixes for migration 77) would have all migrations +from 77 upwards run for the first time. This could be problematic for users who had used the wallet on versions 10.24.0-10.34.0, where our controllers would be writing data to state under the assumption that +the migrations had run. + +As such, we had to also add code to migration 77 to avoid new errors being introduced by the migrations running on code that had been modified by controllers on versions 10.24.0 to 10.34.0 + +## Introducing migration 77 supplements + +To correct the aforementioned problems with the data, new state transformation functions were added to this directory, to be run in migration 77 after the pre-existing migration 77 code had run. +Each file in this directory exports a state transformation function which is then imported in the migration 77 file and applied to state in sequence, after the state transformation function in +077.js itself has run and returns state. These have been split into their own files for each of use, and so that they could be grouped with this documentation. + +## The chosen supplements + +We added supplements for migrations 82, 84, 86 and 88 for the following reasons and with the following effects -> + +**Migration 82** + +Attempts to convert `frequentRpcListDetail` - an array of objects with network related data - to a `networkConfigurations` object, with the objects that were in the array keyed by a unique id. +If this migration had not run, then (prior to v10.34.0) a user would still have been able to use MetaMask, but the data they had in `frequentRpcListDetail` would now be ignored by application code, +and subsequent network data would between written to and modified in state in the `networkConfigurations` object. If migration 82 was later run (after fixing migration 77), the old `frequentRpcListDetail` +data could overwrite the existing `networkConfigurations` data, and the user could lose `networkConfigurations` data that had been written to their state since migration 82 had first failed to run. + +To fix this, the migration 82 supplement deletes `frequentRpcListDetail` if the `networkConfigurations` object exists. Users in such a scenario will have network data in `networkConfigurations` that +they have been using, while the `frequentRpcListDetail` data would not have been seen for some time. So the best thing to do for them is delete their old data and preserve the data they have most recently +used. + +**Migration 84** + +Replaces the `NetworkController.network` property with a `networkId` and `networkStatus` property. If this migration had not run, the NetworkController would have a `network` property and +`networkId` and `networkStatus` properties. If migration 84 later ran on this state, the old (and forgotten) `network` property could cause the `networkId` and `networkStatus` to be overwritten, +affecting the state the user's experience was currently depending on. + +The fix in the migration 84 supplement is to delete the `NetworkController.network` property if the `NetworkId` property already exists. + +**Migration 86** + +Renamed the `NetworkController.provider` property to `providerConfig`. If this migration had not run, the NetworkController would have a `provider` property and +a `providerConfig` property. If migration 86 later ran on this state, the old (and forgotten) `provider` property could cause the `providerConfig` property to be overwritten, +affecting the state the user's experience was currently depending on. + +The fix in the migration 86 supplement is to delete the `NetworkController.provider` property if the `providerConfig` property already exists. + +**Migration 88** + +Attempted to change the keys of multiple parts of state related to tokens. In particular, `NftController.allNftContracts`, `NftController.allNfts`, `TokenListController.tokensChainsCache`, `TokensController.allTokens`, `TokensController.allIgnoredTokens` and `TokensController.allDetectedTokens`. All of these objects were keyed by chainId in decimal number form. The migration's +purpose was to change those decimal chain ID keys to hexadecimal. If migration 77 failed, and then the user added or modified tokens, they could have duplicates within these parts of state: +some with decimal keys and others with an equivalent hexadecimal key. If the data pointed to by those keys was modified at all, and the migration 88 was later run, the most recent data (under +the hexadecimal key) could be overwritten by the old data under the decimal key. + +The migration 88 supplement fixes this by deleting the properties with decimal keys if an equivalent hexadecimal key exists. + +## Migrations that were not supplemented + +**Migration 78** was not supplemented because it only deletes data; it does not overwrite data. It's failure to run will have left rogue data in state, but that will be removed when it is run after the migration +77 fix. + +**Migration 79** was not supplemented because it only deletes data; it does not overwrite data. + +**Migration 80** was not supplemented because it only deletes data; it does not overwrite data. + +**Migration 81** was not supplemented because it modifies data that could only be in state on a flask build. The bug that caused the undefined data in tokenlistcontroller state was present on v14.0.2 and v14.1.0 of +the controllers, but fixed in v14.2.0 of the controllers. By the time flask was released to prod, controllers was at v25.0 + +**Migration 83** just builds on migration 82. No additional fix is needed for 83 given that we have the 82 supplement. + +**Migration 85** was not supplemented because it only deletes data; it does not overwrite data. + +**Migration 87** was not supplemented because it only deletes data; it does not overwrite data. + +**Migration 89** just builds on migration 82 and 84. No additional fix is needed for 89 given that we have the 82 and 84 supplement. + +**Migration 90** was not supplemented because it only deletes data; it does not overwrite data. diff --git a/app/scripts/migrations/077.js b/app/scripts/migrations/077.js index 141cbb142..5a5d4f1ac 100644 --- a/app/scripts/migrations/077.js +++ b/app/scripts/migrations/077.js @@ -1,4 +1,8 @@ import { cloneDeep } from 'lodash'; +import transformState077For082 from './077-supplements/077-supplement-for-082'; +import transformState077For084 from './077-supplements/077-supplement-for-084'; +import transformState077For086 from './077-supplements/077-supplement-for-086'; +import transformState077For088 from './077-supplements/077-supplement-for-088'; const version = 77; @@ -12,7 +16,13 @@ export default { const versionedData = cloneDeep(originalVersionedData); versionedData.meta.version = version; const state = versionedData.data; - const newState = transformState(state); + let newState = transformState(state); + + newState = transformState077For082(newState); + newState = transformState077For084(newState); + newState = transformState077For086(newState); + newState = transformState077For088(newState); + versionedData.data = newState; return versionedData; }, @@ -27,7 +37,7 @@ function transformState(state) { let dataObject; // eslint-disable-next-line for (const chainId in tokensChainsCache) { - dataCache = tokensChainsCache[chainId].data; + dataCache = tokensChainsCache[chainId].data || {}; dataObject = {}; // if the data is array conver that to object if (Array.isArray(dataCache)) { @@ -35,8 +45,8 @@ function transformState(state) { dataObject[token.address] = token; } } else if ( - Object.keys(dataCache)[0].toLowerCase() !== - dataCache[Object.keys(dataCache)[0]].address.toLowerCase() + Object.keys(dataCache)[0]?.toLowerCase() !== + dataCache[Object.keys(dataCache)[0]]?.address?.toLowerCase() ) { // for the users who already updated to the recent version // and the dataCache is already an object keyed with 0,1,2,3 etc diff --git a/app/scripts/migrations/077.test.js b/app/scripts/migrations/077.test.js index 1c16420ec..53efb5cd5 100644 --- a/app/scripts/migrations/077.test.js +++ b/app/scripts/migrations/077.test.js @@ -82,6 +82,100 @@ describe('migration #77', () => { }, }); }); + it('should set data to an empty object if it is undefined', async () => { + const oldStorage = { + meta: { + version: 76, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: undefined, + }, + }, + }, + }, + }; + const newStorage = await migration77.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 77, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: {}, + }, + }, + }, + }, + }); + }); + it('should set data to an empty object if it is null', async () => { + const oldStorage = { + meta: { + version: 76, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: null, + }, + }, + }, + }, + }; + const newStorage = await migration77.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 77, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: {}, + }, + }, + }, + }, + }); + }); it('should change the data from array to object for a multiple networks', async () => { const oldStorage = { meta: { @@ -319,4 +413,1157 @@ describe('migration #77', () => { }, }); }); + + describe('migration #77 supplements', () => { + describe('state transformation to ahead of migration 82', () => { + it('should delete frequentRpcListDetail from the PreferencesController state, if the user already has networkConfigurations in NetworkController state, without interferring with the rest of the migration', async () => { + const oldStorage = { + meta: { + version: 76, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: [ + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + ], + }, + }, + }, + PreferencesController: { + frequentRpcListDetail: ['foobar'], + fizz: 'buzz', + }, + NetworkController: { + networkConfigurations: { foo: 'bar' }, + }, + }, + }; + const newStorage = await migration77.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 77, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + '0xc00e94cb662c3520282e6f5717214004a7f26888': { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + }, + }, + }, + }, + PreferencesController: { + fizz: 'buzz', + }, + NetworkController: { + networkConfigurations: { foo: 'bar' }, + }, + }, + }); + }); + + it('should not delete frequentRpcListDetail from the PreferencesController state if there are no networkConfigurations in NetworkController state', async () => { + const oldStorage = { + meta: { + version: 76, + }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + PreferencesController: { + frequentRpcListDetail: ['foobar'], + fizz: 'buzz', + }, + NetworkController: { + foobar: { foo: 'bar' }, + }, + }, + }; + const newStorage = await migration77.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 77, + }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + PreferencesController: { + frequentRpcListDetail: ['foobar'], + fizz: 'buzz', + }, + NetworkController: { + foobar: { foo: 'bar' }, + }, + }, + }); + }); + }); + + describe('state transformation to ahead of migration 84', () => { + it('should delete `network` from the NetworkController state, if the user already has `networkId` in NetworkController state, without interferring with the rest of the migration', async () => { + const oldStorage = { + meta: { + version: 76, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: [ + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + ], + }, + }, + }, + NetworkController: { + network: 'foobar', + networkId: 'fizzbuzz', + }, + }, + }; + const newStorage = await migration77.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 77, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + '0xc00e94cb662c3520282e6f5717214004a7f26888': { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + }, + }, + }, + }, + NetworkController: { + networkId: 'fizzbuzz', + }, + }, + }); + }); + + it('should not delete `network` from the NetworkController state, if there is no `networkId` in NetworkController state', async () => { + const oldStorage = { + meta: { + version: 76, + }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + PreferencesController: { + frequentRpcListDetail: ['foobar'], + fizz: 'buzz', + }, + NetworkController: { + network: 'foobar', + foobar: { foo: 'bar' }, + }, + }, + }; + const newStorage = await migration77.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 77, + }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + PreferencesController: { + frequentRpcListDetail: ['foobar'], + fizz: 'buzz', + }, + NetworkController: { + network: 'foobar', + foobar: { foo: 'bar' }, + }, + }, + }); + }); + }); + + describe('state transformation to ahead of migration 86', () => { + it('should delete `provider` from the NetworkController state, if the user already has `providerConfig` in NetworkController state, without interferring with the rest of the migration', async () => { + const oldStorage = { + meta: { + version: 76, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: [ + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + ], + }, + }, + }, + NetworkController: { + provider: { foo: 'bar ' }, + providerConfig: { fizz: 'buzz' }, + }, + }, + }; + const newStorage = await migration77.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 77, + }, + data: { + TokenListController: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + }, + tokensChainsCache: { + 1: { + timestamp: 1234, + data: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + '0xc00e94cb662c3520282e6f5717214004a7f26888': { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + }, + }, + }, + }, + NetworkController: { + providerConfig: { fizz: 'buzz' }, + }, + }, + }); + }); + + it('should not delete `provider` from the NetworkController state, if there is no `providerConfig` in NetworkController state', async () => { + const oldStorage = { + meta: { + version: 76, + }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + PreferencesController: { + frequentRpcListDetail: ['foobar'], + fizz: 'buzz', + }, + NetworkController: { + provider: { foo: 'bar ' }, + }, + }, + }; + const newStorage = await migration77.migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 77, + }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + PreferencesController: { + frequentRpcListDetail: ['foobar'], + fizz: 'buzz', + }, + NetworkController: { + provider: { foo: 'bar ' }, + }, + }, + }); + }); + }); + + describe('state transformation to ahead of migration 88', () => { + it('deletes entries in NftController.allNftContracts that have decimal chain ID keys only if any chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 76 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + NftController: { + allNftContracts: { + '0x111': { + 16: [ + { + name: 'Contract 1', + address: '0xaaa', + }, + ], + '0x20': [ + { + name: 'Contract 2', + address: '0xbbb', + }, + ], + 32: [ + { + name: 'Contract 2', + address: '0xbbb', + }, + ], + }, + '0x222': { + 64: [ + { + name: 'Contract 3', + address: '0xccc', + }, + ], + '0x40': [ + { + name: 'Contract 3', + address: '0xccc', + }, + ], + 128: [ + { + name: 'Contract 4', + address: '0xddd', + }, + ], + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + NftController: { + allNftContracts: { + '0x111': { + '0x20': [ + { + name: 'Contract 2', + address: '0xbbb', + }, + ], + }, + '0x222': { + '0x40': [ + { + name: 'Contract 3', + address: '0xccc', + }, + ], + }, + }, + }, + }); + }); + + it('does not delete entries in NftController.allNftContracts that have decimal chain ID keys if no other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 76 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + NftController: { + allNftContracts: { + '0x333': { + 256: [ + { + name: 'Contract 3', + address: '0xccc', + }, + ], + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + NftController: { + allNftContracts: { + '0x333': { + 256: [ + { + name: 'Contract 3', + address: '0xccc', + }, + ], + }, + }, + }, + }); + }); + + it('deletes entries in NftController.allNfts that have decimal chain ID keys only if any chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 76 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + NftController: { + allNfts: { + '0x111': { + 16: [ + { + name: 'NFT 1', + description: 'Description for NFT 1', + image: 'nft1.jpg', + standard: 'ERC721', + tokenId: '1', + address: '0xaaa', + }, + ], + 32: [ + { + name: 'NFT 2', + description: 'Description for NFT 2', + image: 'nft2.jpg', + standard: 'ERC721', + tokenId: '2', + address: '0xbbb', + }, + ], + '0x20': [ + { + name: 'NFT 2', + description: 'Description for NFT 2', + image: 'nft2.jpg', + standard: 'ERC721', + tokenId: '2', + address: '0xbbb', + }, + ], + }, + '0x222': { + 64: [ + { + name: 'NFT 3', + description: 'Description for NFT 3', + image: 'nft3.jpg', + standard: 'ERC721', + tokenId: '3', + address: '0xccc', + }, + ], + '0x40': [ + { + name: 'NFT 3', + description: 'Description for NFT 3', + image: 'nft3.jpg', + standard: 'ERC721', + tokenId: '3', + address: '0xccc', + }, + ], + 128: [ + { + name: 'NFT 4', + description: 'Description for NFT 4', + image: 'nft4.jpg', + standard: 'ERC721', + tokenId: '4', + address: '0xddd', + }, + ], + }, + '0x333': { + 256: [ + { + name: 'NFT 3', + description: 'Description for NFT 3', + image: 'nft3.jpg', + standard: 'ERC721', + tokenId: '3', + address: '0xccc', + }, + ], + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + NftController: { + allNfts: { + '0x111': { + '0x20': [ + { + name: 'NFT 2', + description: 'Description for NFT 2', + image: 'nft2.jpg', + standard: 'ERC721', + tokenId: '2', + address: '0xbbb', + }, + ], + }, + '0x222': { + '0x40': [ + { + name: 'NFT 3', + description: 'Description for NFT 3', + image: 'nft3.jpg', + standard: 'ERC721', + tokenId: '3', + address: '0xccc', + }, + ], + }, + '0x333': { + 256: [ + { + name: 'NFT 3', + description: 'Description for NFT 3', + image: 'nft3.jpg', + standard: 'ERC721', + tokenId: '3', + address: '0xccc', + }, + ], + }, + }, + }, + }); + }); + + it('does not delete entries in NftController.allNfts that have decimal chain ID keys if no other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 76 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + NftController: { + allNfts: { + '0x333': { + 256: [ + { + name: 'NFT 3', + description: 'Description for NFT 3', + image: 'nft3.jpg', + standard: 'ERC721', + tokenId: '3', + address: '0xccc', + }, + ], + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + NftController: { + allNfts: { + '0x333': { + 256: [ + { + name: 'NFT 3', + description: 'Description for NFT 3', + image: 'nft3.jpg', + standard: 'ERC721', + tokenId: '3', + address: '0xccc', + }, + ], + }, + }, + }, + }); + }); + + it('deletes entries in TokenListController.tokensChainsCache that have decimal chain ID keys only if any other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 76 }, + data: { + TokenListController: { + tokensChainsCache: { + 16: { + timestamp: 111111, + data: [ + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + ], + }, + '0x10': { + timestamp: 111111, + data: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + '0xc00e94cb662c3520282e6f5717214004a7f26888': { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + }, + }, + 32: { + timestamp: 222222, + data: [ + { + address: '0x3ee2200efb3400fabb9aacf31297cbdd1d435d47', + symbol: 'ADA', + decimals: 18, + }, + { + address: '0x928e55dab735aa8260af3cedada18b5f70c72f1b', + symbol: 'FRONT', + decimals: 18, + }, + ], + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: { + '0x10': { + timestamp: 111111, + data: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + '0xc00e94cb662c3520282e6f5717214004a7f26888': { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + }, + }, + }, + }, + }); + }); + it('does not delete entries in TokenListController.tokensChainsCache that have decimal chain ID keys if no other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 76 }, + data: { + TokenListController: { + tokensChainsCache: { + 16: { + timestamp: 111111, + data: [ + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + ], + }, + 32: { + timestamp: 222222, + data: [ + { + address: '0x3ee2200efb3400fabb9aacf31297cbdd1d435d47', + symbol: 'ADA', + decimals: 18, + }, + { + address: '0x928e55dab735aa8260af3cedada18b5f70c72f1b', + symbol: 'FRONT', + decimals: 18, + }, + ], + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: { + 16: { + timestamp: 111111, + data: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + }, + '0xc00e94cb662c3520282e6f5717214004a7f26888': { + address: '0xc00e94cb662c3520282e6f5717214004a7f26888', + symbol: 'COMP', + decimals: 18, + }, + }, + }, + 32: { + timestamp: 222222, + data: { + '0x3ee2200efb3400fabb9aacf31297cbdd1d435d47': { + address: '0x3ee2200efb3400fabb9aacf31297cbdd1d435d47', + symbol: 'ADA', + decimals: 18, + }, + '0x928e55dab735aa8260af3cedada18b5f70c72f1b': { + address: '0x928e55dab735aa8260af3cedada18b5f70c72f1b', + symbol: 'FRONT', + decimals: 18, + }, + }, + }, + }, + }, + }); + }); + it('deletes entries in TokensController.allTokens that have decimal chain IDs only if any other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 76 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allTokens: { + 16: { + '0x111': [ + { + address: '0xaaa', + decimals: 1, + symbol: 'TEST1', + }, + ], + }, + '0x10': { + '0x111': [ + { + address: '0xaaa', + decimals: 1, + symbol: 'TEST1', + }, + ], + }, + 32: { + '0x222': [ + { + address: '0xbbb', + decimals: 1, + symbol: 'TEST2', + }, + ], + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allTokens: { + '0x10': { + '0x111': [ + { + address: '0xaaa', + decimals: 1, + symbol: 'TEST1', + }, + ], + }, + }, + }, + }); + }); + + it('does not delete entries in TokensController.allTokens that have decimal chain IDs if no other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 76 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allTokens: { + 16: { + '0x111': [ + { + address: '0xaaa', + decimals: 1, + symbol: 'TEST1', + }, + ], + }, + 32: { + '0x222': [ + { + address: '0xbbb', + decimals: 1, + symbol: 'TEST2', + }, + ], + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allTokens: { + 16: { + '0x111': [ + { + address: '0xaaa', + decimals: 1, + symbol: 'TEST1', + }, + ], + }, + 32: { + '0x222': [ + { + address: '0xbbb', + decimals: 1, + symbol: 'TEST2', + }, + ], + }, + }, + }, + }); + }); + + it('deletes entries in TokensController.allIgnoredTokens that have decimal chain IDs only if any other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 87 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allIgnoredTokens: { + 16: { + '0x1': { + '0x111': ['0xaaa'], + }, + }, + '0x10': { + '0x1': { + '0x222': ['0xbbb'], + }, + }, + 32: { + '0x2': { + '0x222': ['0xbbb'], + }, + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allIgnoredTokens: { + '0x10': { + '0x1': { + '0x222': ['0xbbb'], + }, + }, + }, + }, + }); + }); + + it('does not delete entries in TokensController.allIgnoredTokens that have decimal chain IDs if no other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 87 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allIgnoredTokens: { + 16: { + '0x1': { + '0x111': ['0xaaa'], + }, + }, + 32: { + '0x2': { + '0x222': ['0xbbb'], + }, + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allIgnoredTokens: { + 16: { + '0x1': { + '0x111': ['0xaaa'], + }, + }, + 32: { + '0x2': { + '0x222': ['0xbbb'], + }, + }, + }, + }, + }); + }); + + it('deletes entries in TokensController.allDetectedTokens that have decimal chain IDs only if any other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 87 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allDetectedTokens: { + 16: { + '0x1': { + '0x111': ['0xaaa'], + }, + }, + '0x10': { + '0x1': { + '0x222': ['0xbbb'], + }, + }, + 32: { + '0x2': { + '0x222': ['0xbbb'], + }, + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allDetectedTokens: { + '0x10': { + '0x1': { + '0x222': ['0xbbb'], + }, + }, + }, + }, + }); + }); + + it('does not delete entries in TokensController.allDetectedTokens that have decimal chain IDs if no other chain ID keys are hex', async () => { + const oldStorage = { + meta: { version: 87 }, + data: { + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allDetectedTokens: { + 16: { + '0x1': { + '0x111': ['0xaaa'], + }, + }, + 32: { + '0x2': { + '0x222': ['0xbbb'], + }, + }, + }, + }, + }, + }; + + const newStorage = await migration77.migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + TokenListController: { + tokensChainsCache: {}, + }, + TokensController: { + allDetectedTokens: { + 16: { + '0x1': { + '0x111': ['0xaaa'], + }, + }, + 32: { + '0x2': { + '0x222': ['0xbbb'], + }, + }, + }, + }, + }); + }); + }); + }); });