From f37edd700846e97fa9372cadc01776a0edb75e84 Mon Sep 17 00:00:00 2001 From: tommasini Date: Fri, 8 Mar 2024 21:24:29 +0000 Subject: [PATCH] copy changes from https://github.com/MetaMask/metamask-mobile/pull/8886 --- app/store/migrations/029.test.ts | 91 +++++++++- app/store/migrations/029.ts | 204 ++++++++++++++++++++- app/store/migrations/030.test.ts | 12 +- app/store/migrations/030.ts | 3 +- app/store/migrations/031.ts | 272 ++++++++++++++++++++++++++++ app/store/migrations/031.ts.test.ts | 239 ++++++++++++++++++++++++ app/store/migrations/index.ts | 2 + 7 files changed, 810 insertions(+), 13 deletions(-) create mode 100644 app/store/migrations/031.ts create mode 100644 app/store/migrations/031.ts.test.ts diff --git a/app/store/migrations/029.test.ts b/app/store/migrations/029.test.ts index c0b5b0bf6af..64245f71aed 100644 --- a/app/store/migrations/029.test.ts +++ b/app/store/migrations/029.test.ts @@ -74,6 +74,19 @@ const oldState = { }, ], }, + TokensController: { + allTokens: { '1': { '0x123': { address: '0x123' } } }, + allIgnoredTokens: { '1': { '0x123': { address: '0x123' } } }, + allDetectedTokens: { '1': { '0x123': { address: '0x123' } } }, + }, + TokenRatesController: { + contractExchangeRatesByChainId: { '1': { ETH: { '0x123': 0.0001 } } }, + }, + TokenListController: { + tokensChainsCache: { + '1': { timeStamp: 1, data: { '0x123': { address: '0x123' } } }, + }, + }, }, }, networkOnboarded: { @@ -160,6 +173,19 @@ const expectedNewState = { }, ], }, + TokensController: { + allTokens: { '0x1': { '0x123': { address: '0x123' } } }, + allIgnoredTokens: { '0x1': { '0x123': { address: '0x123' } } }, + allDetectedTokens: { '0x1': { '0x123': { address: '0x123' } } }, + }, + TokenRatesController: { + contractExchangeRatesByChainId: { '0x1': { ETH: { '0x123': 0.0001 } } }, + }, + TokenListController: { + tokensChainsCache: { + '0x1': { timeStamp: 1, data: { '0x123': { address: '0x123' } } }, + }, + }, }, }, networkOnboarded: { @@ -360,11 +386,68 @@ describe('Migration #29', () => { errorMessage: "Migration 29: Invalid TransactionController state: 'null'", scenario: 'TransactionController state is invalid', }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: { + ...initialBackgroundState, + NetworkController: { + ...initialBackgroundState.NetworkController, + networkDetails: { + isEIP1559Compatible: true, + }, + networkConfigurations: {}, + }, + TokensController: null, + }, + }, + }), + errorMessage: "Migration 29: Invalid TokensController state: 'null'", + scenario: 'TokensController state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: { + ...initialBackgroundState, + NetworkController: { + ...initialBackgroundState.NetworkController, + networkDetails: { + isEIP1559Compatible: true, + }, + networkConfigurations: {}, + }, + TokenRatesController: null, + }, + }, + }), + errorMessage: "Migration 29: Invalid TokenRatesController state: 'null'", + scenario: 'TokenRatesController state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: { + ...initialBackgroundState, + NetworkController: { + ...initialBackgroundState.NetworkController, + networkDetails: { + isEIP1559Compatible: true, + }, + networkConfigurations: {}, + }, + TokenListController: null, + }, + }, + }), + errorMessage: "Migration 29: Invalid TokenListController state: 'null'", + scenario: 'TokenListController state is invalid', + }, ]; for (const { errorMessage, scenario, state } of invalidStates) { - it(`should capture exception if ${scenario}`, () => { - const newState = migration(state); + it(`should capture exception if ${scenario}`, async () => { + const newState = await migration(state); expect(newState).toStrictEqual(state); expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); @@ -374,8 +457,8 @@ describe('Migration #29', () => { }); } - it('All states changing as expected', () => { - const newState = migration(oldState); + it('All states changing as expected', async () => { + const newState = await migration(oldState); expect(newState).toStrictEqual(expectedNewState); }); diff --git a/app/store/migrations/029.ts b/app/store/migrations/029.ts index 29d636a283a..4a33dd35661 100644 --- a/app/store/migrations/029.ts +++ b/app/store/migrations/029.ts @@ -11,7 +11,14 @@ import { AddressBookEntry, AddressBookState, } from '@metamask/address-book-controller'; -import { Nft, NftContract, NftState } from '@metamask/assets-controllers'; +import { + Nft, + NftContract, + NftState, + TokenListState, + TokenRatesState, + TokensState, +} from '@metamask/assets-controllers'; /** * Converting chain id on decimal format to hexadecimal format @@ -28,7 +35,8 @@ import { Nft, NftContract, NftState } from '@metamask/assets-controllers'; * @param {any} state - Redux state. * @returns Migrated Redux state. */ -export default function migrate(state: unknown) { +export default async function migrate(stateAsync: unknown) { + const state = await stateAsync; // Chaning chain id to hexadecimal chain Id on the networks already on the local state if (!isObject(state)) { captureException( @@ -484,5 +492,197 @@ export default function migrate(state: unknown) { ); } + const tokenListControllerState = + state?.engine?.backgroundState?.TokenListController; + const newTokenListControllerState = state?.engine?.backgroundState + ?.TokenListController as TokenListState; + + if (!isObject(tokenListControllerState)) { + captureException( + new Error( + `Migration 29: Invalid TokenListController state: '${JSON.stringify( + tokenListControllerState, + )}'`, + ), + ); + return state; + } + + if ( + !hasProperty(tokenListControllerState, 'tokensChainsCache') || + !isObject(tokenListControllerState.tokensChainsCache) + ) { + captureException( + new Error( + `Migration 29: Invalid tokenListControllerState tokensChainsCache: '${JSON.stringify( + tokenListControllerState.tokensChainsCache, + )}'`, + ), + ); + return state; + } + + if (Object.keys(tokenListControllerState.tokensChainsCache).length) { + Object.keys(tokenListControllerState.tokensChainsCache).forEach( + (chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + newTokenListControllerState.tokensChainsCache[hexChainId] = + //@ts-expect-error Is verified on Line 508 that tokenChainsCache is a property + tokenListControllerState.tokensChainsCache[chainId]; + + if (isObject(tokenListControllerState.tokensChainsCache)) { + delete tokenListControllerState.tokensChainsCache[chainId]; + } + } + }, + ); + } + + const tokenRatesControllerState = + state?.engine?.backgroundState?.TokenRatesController; + const newTokenRatesControllerState = state?.engine?.backgroundState + ?.TokenRatesController as TokenRatesState; + + if (!isObject(tokenRatesControllerState)) { + captureException( + new Error( + `Migration 29: Invalid TokenRatesController state: '${JSON.stringify( + tokenRatesControllerState, + )}'`, + ), + ); + return state; + } + + if ( + isObject(tokenRatesControllerState.contractExchangeRatesByChainId) && + Object.keys(tokenRatesControllerState.contractExchangeRatesByChainId).length + ) { + Object.keys( + tokenRatesControllerState.contractExchangeRatesByChainId, + ).forEach((chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + newTokenRatesControllerState.contractExchangeRatesByChainId[ + hexChainId + ] = + //@ts-expect-error Is verified on Line 558 that contractExchangeRatesByChainId is a property + tokenRatesControllerState.contractExchangeRatesByChainId[chainId]; + + if ( + isObject(tokenRatesControllerState.contractExchangeRatesByChainId) + ) { + delete tokenRatesControllerState.contractExchangeRatesByChainId[ + chainId + ]; + } + } + }); + } + + const tokensControllerState = + state?.engine?.backgroundState?.TokensController; + const newTokensControllerState = state?.engine?.backgroundState + ?.TokensController as TokensState; + + if (!isObject(tokensControllerState)) { + captureException( + new Error( + `Migration 29: Invalid TokensController state: '${JSON.stringify( + tokensControllerState, + )}'`, + ), + ); + return state; + } + + if ( + !hasProperty(tokensControllerState, 'allTokens') || + !isObject(tokensControllerState.allTokens) + ) { + captureException( + new Error( + `Migration 29: Invalid TokensController allTokens: '${JSON.stringify( + tokensControllerState.allTokens, + )}'`, + ), + ); + return state; + } + + if (Object.keys(tokensControllerState.allTokens).length) { + Object.keys(tokensControllerState.allTokens).forEach((chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + newTokensControllerState.allTokens[hexChainId] = + //@ts-expect-error Is verified on Line 613 that allTokens is a property + tokensControllerState.allTokens[chainId]; + + if (isObject(tokensControllerState.allTokens)) { + delete tokensControllerState.allTokens[chainId]; + } + } + }); + } + + if ( + !hasProperty(tokensControllerState, 'allIgnoredTokens') || + !isObject(tokensControllerState.allIgnoredTokens) + ) { + captureException( + new Error( + `Migration 29: Invalid TokensController allIgnoredTokens: '${JSON.stringify( + tokensControllerState.allIgnoredTokens, + )}'`, + ), + ); + return state; + } + + if (Object.keys(tokensControllerState.allIgnoredTokens).length) { + Object.keys(tokensControllerState.allIgnoredTokens).forEach((chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + newTokensControllerState.allIgnoredTokens[hexChainId] = + //@ts-expect-error Is verified on Line 643 that allIgnoredTokens is a property + tokensControllerState.allIgnoredTokens[chainId]; + + if (isObject(tokensControllerState.allIgnoredTokens)) { + delete tokensControllerState.allIgnoredTokens[chainId]; + } + } + }); + } + + if ( + !hasProperty(tokensControllerState, 'allDetectedTokens') || + !isObject(tokensControllerState.allDetectedTokens) + ) { + captureException( + new Error( + `Migration 29: Invalid TokensController allDetectedTokens: '${JSON.stringify( + tokensControllerState.allDetectedTokens, + )}'`, + ), + ); + return state; + } + + if (Object.keys(tokensControllerState.allDetectedTokens).length) { + Object.keys(tokensControllerState.allDetectedTokens).forEach((chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + newTokensControllerState.allDetectedTokens[hexChainId] = + //@ts-expect-error Is verified on Line 671 that allIgnoredTokens is a property + tokensControllerState.allDetectedTokens[chainId]; + + if (isObject(tokensControllerState.allDetectedTokens)) { + delete tokensControllerState.allDetectedTokens[chainId]; + } + } + }); + } + return state; } diff --git a/app/store/migrations/030.test.ts b/app/store/migrations/030.test.ts index 8f404f0a8f6..42950f5b2b0 100644 --- a/app/store/migrations/030.test.ts +++ b/app/store/migrations/030.test.ts @@ -50,8 +50,8 @@ describe('Migration #30', () => { ]; for (const { errorMessage, scenario, state } of invalidStates) { - it(`should capture exception if ${scenario}`, () => { - const newState = migrate(state); + it(`should capture exception if ${scenario}`, async () => { + const newState = await migrate(state); expect(newState).toStrictEqual(state); expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); @@ -61,7 +61,7 @@ describe('Migration #30', () => { }); } - it('should not change anything if security alert is already enabled', () => { + it('should not change anything if security alert is already enabled', async () => { const oldState = { engine: { backgroundState: { @@ -72,11 +72,11 @@ describe('Migration #30', () => { }, }; - const migratedState = migrate(oldState); + const migratedState = await migrate(oldState); expect(migratedState).toStrictEqual(expectedState); }); - it('should enable security alert if it is not enabled', () => { + it('should enable security alert if it is not enabled', async () => { const oldState = { engine: { backgroundState: { @@ -86,7 +86,7 @@ describe('Migration #30', () => { }, }, }; - const migratedState = migrate(oldState); + const migratedState = await migrate(oldState); expect(migratedState).toStrictEqual(expectedState); }); }); diff --git a/app/store/migrations/030.ts b/app/store/migrations/030.ts index dab6dcd4c01..a76bb445768 100644 --- a/app/store/migrations/030.ts +++ b/app/store/migrations/030.ts @@ -7,7 +7,8 @@ import { isObject } from '@metamask/utils'; * @param {any} state - Redux state. * @returns Migrated Redux state. */ -export default function migrate(state: unknown) { +export default async function migrate(stateAsync: unknown) { + const state = await stateAsync; if (!isObject(state)) { captureException( new Error(`Migration 30: Invalid root state: '${typeof state}'`), diff --git a/app/store/migrations/031.ts b/app/store/migrations/031.ts new file mode 100644 index 00000000000..cccf72d1420 --- /dev/null +++ b/app/store/migrations/031.ts @@ -0,0 +1,272 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { captureException } from '@sentry/react-native'; +import { + TokenListState, + TokenRatesState, + TokensState, +} from '@metamask/assets-controllers'; +import { toHex } from '@metamask/controller-utils'; +//@ts-expect-error - This error is expected, but ethereumjs-util exports this function +import { isHexString } from 'ethereumjs-util'; + +/** + * This migration is to address the users that were impacted by the tokens missing on their wallet + * Because the chain id was not migrated to hexadecimal format + * And still didn't import the tokens again + * @param state + * @returns + */ +export default async function migrate(stateAsync: unknown) { + const state = await stateAsync; + if (!isObject(state)) { + captureException( + new Error(`Migration 31: Invalid state: '${typeof state}'`), + ); + return state; + } + + if (!isObject(state.engine)) { + captureException( + new Error(`Migration 31: Invalid engine state: '${typeof state.engine}'`), + ); + return state; + } + + if (!isObject(state.engine.backgroundState)) { + captureException( + new Error( + `Migration 31: Invalid engine backgroundState: '${typeof state.engine + .backgroundState}'`, + ), + ); + return state; + } + + const tokenListControllerState = + state?.engine?.backgroundState?.TokenListController; + const newTokenListControllerState = state?.engine?.backgroundState + ?.TokenListController as TokenListState; + + if (!isObject(tokenListControllerState)) { + captureException( + new Error( + `Migration 31: Invalid TokenListController state: '${JSON.stringify( + tokenListControllerState, + )}'`, + ), + ); + return state; + } + + if ( + !hasProperty(tokenListControllerState, 'tokensChainsCache') || + !isObject(tokenListControllerState.tokensChainsCache) + ) { + captureException( + new Error( + `Migration 31: Invalid tokenListControllerState tokensChainsCache: '${JSON.stringify( + tokenListControllerState.tokensChainsCache, + )}'`, + ), + ); + return state; + } + + if (Object.keys(tokenListControllerState.tokensChainsCache).length) { + Object.keys(tokenListControllerState.tokensChainsCache).forEach( + (chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + + if ( + !Object.prototype.hasOwnProperty.call( + newTokenListControllerState.tokensChainsCache, + hexChainId, + ) + ) { + newTokenListControllerState.tokensChainsCache[hexChainId] = + //@ts-expect-error Is verified on Line 508 that tokenChainsCache is a property + tokenListControllerState.tokensChainsCache[chainId]; + } + + if (isObject(tokenListControllerState.tokensChainsCache)) { + delete tokenListControllerState.tokensChainsCache[chainId]; + } + } + }, + ); + } + + const tokenRatesControllerState = + state?.engine?.backgroundState?.TokenRatesController; + const newTokenRatesControllerState = state?.engine?.backgroundState + ?.TokenRatesController as TokenRatesState; + + if (!isObject(tokenRatesControllerState)) { + captureException( + new Error( + `Migration 31: Invalid TokenRatesController state: '${JSON.stringify( + tokenRatesControllerState, + )}'`, + ), + ); + return state; + } + + if ( + isObject(tokenRatesControllerState.contractExchangeRatesByChainId) && + Object.keys(tokenRatesControllerState.contractExchangeRatesByChainId).length + ) { + Object.keys( + tokenRatesControllerState.contractExchangeRatesByChainId, + ).forEach((chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + + if ( + !Object.prototype.hasOwnProperty.call( + newTokenRatesControllerState.contractExchangeRatesByChainId, + hexChainId, + ) + ) { + newTokenRatesControllerState.contractExchangeRatesByChainId[ + hexChainId + ] = + //@ts-expect-error Is verified on Line 558 that contractExchangeRatesByChainId is a property + tokenRatesControllerState.contractExchangeRatesByChainId[chainId]; + } + + if ( + isObject(tokenRatesControllerState.contractExchangeRatesByChainId) + ) { + delete tokenRatesControllerState.contractExchangeRatesByChainId[ + chainId + ]; + } + } + }); + } + + const tokensControllerState = + state?.engine?.backgroundState?.TokensController; + const newTokensControllerState = state?.engine?.backgroundState + ?.TokensController as TokensState; + + if (!isObject(tokensControllerState)) { + captureException( + new Error( + `Migration 31: Invalid TokensController state: '${JSON.stringify( + tokensControllerState, + )}'`, + ), + ); + return state; + } + + if ( + !hasProperty(tokensControllerState, 'allTokens') || + !isObject(tokensControllerState.allTokens) + ) { + captureException( + new Error( + `Migration 31: Invalid TokensController allTokens: '${JSON.stringify( + tokensControllerState.allTokens, + )}'`, + ), + ); + return state; + } + + if (Object.keys(tokensControllerState.allTokens).length) { + Object.keys(tokensControllerState.allTokens).forEach((chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + if ( + !Object.prototype.hasOwnProperty.call( + newTokensControllerState.allTokens, + hexChainId, + ) + ) { + newTokensControllerState.allTokens[hexChainId] = + //@ts-expect-error Is verified on Line 613 that allTokens is a property + tokensControllerState.allTokens[chainId]; + } + if (isObject(tokensControllerState.allTokens)) { + delete tokensControllerState.allTokens[chainId]; + } + } + }); + } + + if ( + !hasProperty(tokensControllerState, 'allIgnoredTokens') || + !isObject(tokensControllerState.allIgnoredTokens) + ) { + captureException( + new Error( + `Migration 31: Invalid TokensController allIgnoredTokens: '${JSON.stringify( + tokensControllerState.allIgnoredTokens, + )}'`, + ), + ); + return state; + } + + if (Object.keys(tokensControllerState.allIgnoredTokens).length) { + Object.keys(tokensControllerState.allIgnoredTokens).forEach((chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + if ( + !Object.prototype.hasOwnProperty.call( + newTokensControllerState.allIgnoredTokens, + hexChainId, + ) + ) { + newTokensControllerState.allIgnoredTokens[hexChainId] = + //@ts-expect-error Is verified on Line 643 that allIgnoredTokens is a property + tokensControllerState.allIgnoredTokens[chainId]; + } + if (isObject(tokensControllerState.allIgnoredTokens)) { + delete tokensControllerState.allIgnoredTokens[chainId]; + } + } + }); + } + + if ( + !hasProperty(tokensControllerState, 'allDetectedTokens') || + !isObject(tokensControllerState.allDetectedTokens) + ) { + captureException( + new Error( + `Migration 31: Invalid TokensController allDetectedTokens: '${JSON.stringify( + tokensControllerState.allDetectedTokens, + )}'`, + ), + ); + return state; + } + + if (Object.keys(tokensControllerState.allDetectedTokens).length) { + Object.keys(tokensControllerState.allDetectedTokens).forEach((chainId) => { + if (!isHexString(chainId)) { + const hexChainId = toHex(chainId); + if ( + !Object.prototype.hasOwnProperty.call( + newTokensControllerState.allDetectedTokens, + hexChainId, + ) + ) { + newTokensControllerState.allDetectedTokens[hexChainId] = + //@ts-expect-error Is verified on Line 671 that allIgnoredTokens is a property + tokensControllerState.allDetectedTokens[chainId]; + } + if (isObject(tokensControllerState.allDetectedTokens)) { + delete tokensControllerState.allDetectedTokens[chainId]; + } + } + }); + } + + return state; +} diff --git a/app/store/migrations/031.ts.test.ts b/app/store/migrations/031.ts.test.ts new file mode 100644 index 00000000000..35dff395e6c --- /dev/null +++ b/app/store/migrations/031.ts.test.ts @@ -0,0 +1,239 @@ +import migration from './031'; +import { merge } from 'lodash'; +import { captureException } from '@sentry/react-native'; +import initialRootState from '../../util/test/initial-root-state'; +const oldState1 = { + engine: { + backgroundState: { + NetworkController: { + providerConfig: { + type: 'mainnet', + chainId: '0x1', + rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', + ticker: 'ETH', + }, + }, + TokensController: { + allTokens: { + '1': { '0x123': { address: '0x123' }, '0x12': { address: '0x122' } }, + '0x1': { + '0x123': { address: '0x123' }, + '0x1234': { address: '0x1234' }, + }, + }, + allIgnoredTokens: { + '1': { '0x123': { address: '0x123' }, '0x12': { address: '0x122' } }, + '0x1': { + '0x123': { address: '0x123' }, + '0x1234': { address: '0x1234' }, + }, + }, + allDetectedTokens: { + '1': { '0x123': { address: '0x123' }, '0x12': { address: '0x122' } }, + '0x1': { + '0x123': { address: '0x123' }, + '0x1234': { address: '0x1234' }, + }, + }, + }, + TokenRatesController: { + contractExchangeRatesByChainId: { + '1': { ETH: { '0x123': 0.0001 } }, + '0x1': { ETH: { '0x123': 0.0001 } }, + }, + }, + TokenListController: { + tokensChainsCache: { + '1': { timeStamp: 1, data: { '0x123': { address: '0x123' } } }, + '0x1': { timeStamp: 1, data: { '0x123': { address: '0x123' } } }, + }, + }, + }, + }, +}; + +const oldState2 = { + engine: { + backgroundState: { + NetworkController: { + providerConfig: { + type: 'mainnet', + chainId: '0x1', + rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', + ticker: 'ETH', + }, + }, + TokensController: { + allTokens: { + '1': { '0x123': { address: '0x123' }, '0x12': { address: '0x122' } }, + }, + allIgnoredTokens: { + '1': { '0x123': { address: '0x123' }, '0x12': { address: '0x122' } }, + }, + allDetectedTokens: { + '1': { '0x123': { address: '0x123' }, '0x12': { address: '0x122' } }, + }, + }, + TokenRatesController: { + contractExchangeRatesByChainId: { + '1': { ETH: { '0x123': 0.0001 } }, + }, + }, + TokenListController: { + tokensChainsCache: { + '1': { timeStamp: 1, data: { '0x123': { address: '0x123' } } }, + }, + }, + }, + }, +}; + +const expectedState1 = { + engine: { + backgroundState: { + NetworkController: { + providerConfig: { + type: 'mainnet', + chainId: '0x1', + rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', + ticker: 'ETH', + }, + }, + TokensController: { + allTokens: { + '0x1': { + '0x123': { address: '0x123' }, + '0x1234': { address: '0x1234' }, + }, + }, + allIgnoredTokens: { + '0x1': { + '0x123': { address: '0x123' }, + '0x1234': { address: '0x1234' }, + }, + }, + allDetectedTokens: { + '0x1': { + '0x123': { address: '0x123' }, + '0x1234': { address: '0x1234' }, + }, + }, + }, + TokenRatesController: { + contractExchangeRatesByChainId: { + '0x1': { ETH: { '0x123': 0.0001 } }, + }, + }, + TokenListController: { + tokensChainsCache: { + '0x1': { timeStamp: 1, data: { '0x123': { address: '0x123' } } }, + }, + }, + }, + }, +}; + +const expectedState2 = { + engine: { + backgroundState: { + NetworkController: { + providerConfig: { + type: 'mainnet', + chainId: '0x1', + rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', + ticker: 'ETH', + }, + }, + TokensController: { + allTokens: { + '0x1': { + '0x123': { address: '0x123' }, + '0x12': { address: '0x122' }, + }, + }, + allIgnoredTokens: { + '0x1': { + '0x123': { address: '0x123' }, + '0x12': { address: '0x122' }, + }, + }, + allDetectedTokens: { + '0x1': { + '0x123': { address: '0x123' }, + '0x12': { address: '0x122' }, + }, + }, + }, + TokenRatesController: { + contractExchangeRatesByChainId: { + '0x1': { ETH: { '0x123': 0.0001 } }, + }, + }, + TokenListController: { + tokensChainsCache: { + '0x1': { timeStamp: 1, data: { '0x123': { address: '0x123' } } }, + }, + }, + }, + }, +}; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); +const mockedCaptureException = jest.mocked(captureException); + +describe('Migration #33', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + }); + + const invalidStates = [ + { + state: null, + errorMessage: "Migration 31: Invalid state: 'object'", + scenario: 'state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: null, + }), + errorMessage: "Migration 31: Invalid engine state: 'object'", + scenario: 'engine state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: null, + }, + }), + errorMessage: "Migration 31: Invalid engine backgroundState: 'object'", + scenario: 'backgroundState is invalid', + }, + ]; + + for (const { errorMessage, scenario, state } of invalidStates) { + it(`should capture exception if ${scenario}`, async () => { + const newState = await migration(state); + + expect(newState).toStrictEqual(state); + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); + expect(mockedCaptureException.mock.calls[0][0].message).toBe( + errorMessage, + ); + }); + } + + it('should convert to hexadecimal all the chain id values that are on decimal format to hexadecimal format if that hexadecimal value do not exist', async () => { + const newState = await migration(oldState2); + + expect(newState).toStrictEqual(expectedState2); + }); + + it('should delete object that have decimal keys on their data, if the hexadecimal key of that decimal key already exists', async () => { + const newState = await migration(oldState1); + + expect(newState).toStrictEqual(expectedState1); + }); +}); diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index de194ea95db..71fbe714fc0 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -31,6 +31,7 @@ import migration27 from './027'; import migration28 from './028'; import migration29 from './029'; import migration30 from './030'; +import migration31 from './031'; // We do not keep track of the old state // We create this type for better readability @@ -68,6 +69,7 @@ export const migrations: MigrationManifest = { 28: migration28 as unknown as (state: PersistedState) => PersistedState, 29: migration29 as unknown as (state: OldState) => PersistedState, 30: migration30 as unknown as (state: OldState) => PersistedState, + 31: migration31 as unknown as (state: OldState) => PersistedState, }; // The latest (i.e. highest) version number.