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 }; data: Record; }; export const version = 88; /** * This migration does a few things: * * - Rebuilds `allNftContracts` and `allNfts` in NftController state to be keyed * by a hex chain ID rather than a decimal chain ID. * - Rebuilds `tokensChainsCache` in TokenListController to be keyed by a hex * chain ID rather than a decimal chain ID. * - Rebuilds `allTokens`, `allDetectedTokens`, and `allIgnoredTokens` in * TokensController to be keyed by a hex chain ID rather than a decimal chain ID. * - removes any entries in `allNftContracts`, `allNfts`, `tokensChainsCache`, * `allTokens`, `allIgnoredTokens` or `allDetectedTokens` that are keyed by the * string 'undefined' * * @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 { const versionedData = cloneDeep(originalVersionedData); versionedData.meta.version = version; migrateData(versionedData.data); return versionedData; } function migrateData(state: Record): void { 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)) { for (const chainId of Object.keys(nftContractsByChainId)) { if (chainId === 'undefined' || chainId === undefined) { delete nftContractsByChainId[chainId]; } } allNftContracts[address] = mapKeys( nftContractsByChainId, (_, chainId: string) => toHex(chainId), ); } }); } } 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 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)) { for (const chainId of Object.keys(nftsByChainId)) { if (chainId === 'undefined' || chainId === undefined) { delete nftsByChainId[chainId]; } } allNfts[address] = mapKeys(nftsByChainId, (_, chainId: string) => toHex(chainId), ); } }); } } 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 ( hasProperty(state, 'TokenListController') && isObject(state.TokenListController) ) { const tokenListControllerState = state.TokenListController; // Migrate TokenListController.tokensChainsCache if ( hasProperty(tokenListControllerState, 'tokensChainsCache') && isObject(tokenListControllerState.tokensChainsCache) ) { for (const chainId of Object.keys( tokenListControllerState.tokensChainsCache, )) { if (chainId === 'undefined' || chainId === undefined) { delete tokenListControllerState.tokensChainsCache[chainId]; } } tokenListControllerState.tokensChainsCache = mapKeys( 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 { console.warn( new Error( `typeof state.TokenListController is ${typeof state.TokenListController}`, ), ); } if ( hasProperty(state, 'TokensController') && isObject(state.TokensController) ) { const tokensControllerState = state.TokensController; // Migrate TokensController.allTokens if ( hasProperty(tokensControllerState, 'allTokens') && isObject(tokensControllerState.allTokens) ) { const { allTokens } = tokensControllerState; for (const chainId of Object.keys(allTokens)) { if (chainId === 'undefined' || chainId === undefined) { delete allTokens[chainId]; } } tokensControllerState.allTokens = mapKeys( 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 if ( hasProperty(tokensControllerState, 'allIgnoredTokens') && isObject(tokensControllerState.allIgnoredTokens) ) { const { allIgnoredTokens } = tokensControllerState; for (const chainId of Object.keys(allIgnoredTokens)) { if (chainId === 'undefined' || chainId === undefined) { delete allIgnoredTokens[chainId]; } } tokensControllerState.allIgnoredTokens = mapKeys( 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 if ( hasProperty(tokensControllerState, 'allDetectedTokens') && isObject(tokensControllerState.allDetectedTokens) ) { const { allDetectedTokens } = tokensControllerState; for (const chainId of Object.keys(allDetectedTokens)) { if (chainId === 'undefined' || chainId === undefined) { delete allDetectedTokens[chainId]; } } tokensControllerState.allDetectedTokens = mapKeys( 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}`, ), ); } } function toHex(value: number | string | BN): Hex { if (typeof value === 'string' && isStrictHexString(value)) { return value; } const hexString = BN.isBN(value) ? value.toString(16) : new BN(value.toString(), 10).toString(16); return `0x${hexString}`; }