diff --git a/src/hooks/useLoadGlobalLateData.ts b/src/hooks/useLoadGlobalLateData.ts index 2c129fb9b3a..c4d9d41fcfa 100644 --- a/src/hooks/useLoadGlobalLateData.ts +++ b/src/hooks/useLoadGlobalLateData.ts @@ -1,15 +1,14 @@ -import { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { getWalletBalances, WALLET_BALANCES_FROM_STORAGE } from '@/handlers/localstorage/walletBalances'; import { queryClient } from '@/react-query'; -import { AppState } from '@/redux/store'; -import { promiseUtils } from '@/utils'; -import logger from '@/utils/logger'; +import { contactsLoadState } from '@/redux/contacts'; import { imageMetadataCacheLoadState } from '@/redux/imageMetadata'; import { keyboardHeightsLoadState } from '@/redux/keyboardHeight'; +import { AppState } from '@/redux/store'; import { transactionSignaturesLoadState } from '@/redux/transactionSignatures'; -import { contactsLoadState } from '@/redux/contacts'; -import { favoritesQueryKey, refreshFavorites } from '@/resources/favorites'; +import { promiseUtils } from '@/utils'; +import logger from '@/utils/logger'; +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; const loadWalletBalanceNamesToCache = () => queryClient.prefetchQuery([WALLET_BALANCES_FROM_STORAGE], getWalletBalances); @@ -28,12 +27,6 @@ export default function useLoadGlobalLateData() { // mainnet eth balances for all wallets const p2 = loadWalletBalanceNamesToCache(); - // favorites - const p3 = queryClient.prefetchQuery({ - queryKey: favoritesQueryKey, - queryFn: () => refreshFavorites(), - }); - // contacts const p4 = dispatch(contactsLoadState()); @@ -46,7 +39,7 @@ export default function useLoadGlobalLateData() { // tx method names const p7 = dispatch(transactionSignaturesLoadState()); - promises.push(p2, p3, p4, p5, p6, p7); + promises.push(p2, p4, p5, p6, p7); // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(Promise | ((dispatch: Dis... Remove this comment to see the full error message return promiseUtils.PromiseAllWithFails(promises); diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 0639f8f5162..d36aff807f7 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -1,20 +1,21 @@ import { InteractionManager } from 'react-native'; import * as env from '@/env'; -import { Storage } from '@/storage'; import { logger, RainbowError } from '@/logger'; +import { Storage } from '@/storage'; -import { MIGRATIONS_DEBUG_CONTEXT, MIGRATIONS_STORAGE_ID, MigrationName, Migration } from '@/migrations/types'; import { deleteImgixMMKVCache } from '@/migrations/migrations/deleteImgixMMKVCache'; import { migrateNotificationSettingsToV2 } from '@/migrations/migrations/migrateNotificationSettingsToV2'; import { prepareDefaultNotificationGroupSettingsState } from '@/migrations/migrations/prepareDefaultNotificationGroupSettingsState'; +import { Migration, MigrationName, MIGRATIONS_DEBUG_CONTEXT, MIGRATIONS_STORAGE_ID } from '@/migrations/types'; import { changeLanguageKeys } from './migrations/changeLanguageKeys'; import { fixHiddenUSDC } from './migrations/fixHiddenUSDC'; -import { purgeWcConnectionsWithoutAccounts } from './migrations/purgeWcConnectionsWithoutAccounts'; -import { migratePinnedAndHiddenTokenUniqueIds } from './migrations/migratePinnedAndHiddenTokenUniqueIds'; -import { migrateUnlockableAppIconStorage } from './migrations/migrateUnlockableAppIconStorage'; +import { migrateFavoritesToV2 } from './migrations/migrateFavoritesToV2'; import { migratePersistedQueriesToMMKV } from './migrations/migratePersistedQueriesToMMKV'; +import { migratePinnedAndHiddenTokenUniqueIds } from './migrations/migratePinnedAndHiddenTokenUniqueIds'; import { migrateRemotePromoSheetsToZustand } from './migrations/migrateRemotePromoSheetsToZustand'; +import { migrateUnlockableAppIconStorage } from './migrations/migrateUnlockableAppIconStorage'; +import { purgeWcConnectionsWithoutAccounts } from './migrations/purgeWcConnectionsWithoutAccounts'; /** * Local storage for migrations only. Should not be exported. @@ -41,6 +42,7 @@ const migrations: Migration[] = [ migrateUnlockableAppIconStorage(), migratePersistedQueriesToMMKV(), migrateRemotePromoSheetsToZustand(), + migrateFavoritesToV2(), ]; /** diff --git a/src/migrations/migrations/migrateFavoritesToV2.ts b/src/migrations/migrations/migrateFavoritesToV2.ts new file mode 100644 index 00000000000..f9a50dfd96b --- /dev/null +++ b/src/migrations/migrations/migrateFavoritesToV2.ts @@ -0,0 +1,31 @@ +import { AddressOrEth, UniqueId } from '@/__swaps__/types/assets'; +import { getStandardizedUniqueIdWorklet } from '@/__swaps__/utils/swaps'; +import { EthereumAddress, RainbowToken } from '@/entities'; +import { createQueryKey, queryClient } from '@/react-query'; +import { favoritesQueryKey } from '@/resources/favorites'; +import { ethereumUtils } from '@/utils'; +import { Migration, MigrationName } from '../types'; + +export function migrateFavoritesToV2(): Migration { + return { + name: MigrationName.migrateFavoritesToV2, + async migrate() { + // v1 used just the address as key, v2 uses uniqueId as key and builds this uniqueId with ChainId instead of Network + const favoritesV1QueryKey = createQueryKey('favorites', {}, { persisterVersion: 1 }); + const v1Data = queryClient.getQueryData>(favoritesV1QueryKey); + if (v1Data) { + const migratedFavorites: Record = {}; + for (const favorite of Object.values(v1Data)) { + const uniqueId = getStandardizedUniqueIdWorklet({ + address: favorite.address as AddressOrEth, + chainId: ethereumUtils.getChainIdFromNetwork(favorite.network), + }); + favorite.uniqueId = uniqueId; // v2 unique uses chainId instead of Network + migratedFavorites[uniqueId] = favorite; + } + queryClient.setQueryData(favoritesQueryKey, migratedFavorites); + queryClient.setQueryData(favoritesV1QueryKey, undefined); // clear v1 store data + } + }, + }; +} diff --git a/src/migrations/types.ts b/src/migrations/types.ts index 7797a230f51..a21fe98e5e7 100644 --- a/src/migrations/types.ts +++ b/src/migrations/types.ts @@ -18,6 +18,7 @@ export enum MigrationName { migrateUnlockableAppIconStorage = 'migration_migrateUnlockableAppIconStorage', migratePersistedQueriesToMMKV = 'migration_migratePersistedQueriesToMMKV', migrateRemotePromoSheetsToZustand = 'migration_migrateRemotePromoSheetsToZustand', + migrateFavoritesToV2 = 'migration_migrateFavoritesToV2', } export type Migration = { diff --git a/src/resources/favorites.ts b/src/resources/favorites.ts index b8dba93651d..355b9c6c59b 100644 --- a/src/resources/favorites.ts +++ b/src/resources/favorites.ts @@ -1,18 +1,27 @@ -import { EthereumAddress, NativeCurrencyKeys, RainbowToken } from '@/entities'; +import { AddressOrEth, UniqueId } from '@/__swaps__/types/assets'; +import { ChainId } from '@/__swaps__/types/chains'; +import { getStandardizedUniqueIdWorklet } from '@/__swaps__/utils/swaps'; +import { NativeCurrencyKeys, RainbowToken } from '@/entities'; import { Network } from '@/networks/types'; import { createQueryKey, queryClient } from '@/react-query'; import { DAI_ADDRESS, ETH_ADDRESS, SOCKS_ADDRESS, WBTC_ADDRESS, WETH_ADDRESS } from '@/references'; -import ethereumUtils, { getUniqueId } from '@/utils/ethereumUtils'; +import { promiseUtils } from '@/utils'; +import ethereumUtils from '@/utils/ethereumUtils'; import { useQuery } from '@tanstack/react-query'; import { omit } from 'lodash'; import { externalTokenQueryKey, fetchExternalToken } from './assets/externalAssetsQuery'; -import { ChainId } from '@/__swaps__/types/chains'; -import { promiseUtils } from '@/utils'; -export const favoritesQueryKey = createQueryKey('favorites', {}, { persisterVersion: 1 }); +export const favoritesQueryKey = createQueryKey('favorites', {}, { persisterVersion: 2 }); + +const getUniqueId = (address: AddressOrEth, chainId: ChainId) => getStandardizedUniqueIdWorklet({ address, chainId }); -const DEFAULT: Record = { - [DAI_ADDRESS]: { +const DAI_uniqueId = getUniqueId(DAI_ADDRESS, ChainId.mainnet); +const ETH_uniqueId = getUniqueId(ETH_ADDRESS, ChainId.mainnet); +const SOCKS_uniqueId = getUniqueId(SOCKS_ADDRESS, ChainId.mainnet); +const WBTC_uniqueId = getUniqueId(WBTC_ADDRESS, ChainId.mainnet); + +const DEFAULT: Record = { + [DAI_uniqueId]: { address: DAI_ADDRESS, color: '#F0B340', decimals: 18, @@ -23,9 +32,12 @@ const DEFAULT: Record = { name: 'Dai', symbol: 'DAI', network: Network.mainnet, - uniqueId: DAI_ADDRESS, + uniqueId: DAI_uniqueId, + networks: { + [ChainId.mainnet]: { address: DAI_ADDRESS }, + }, }, - [ETH_ADDRESS]: { + [ETH_uniqueId]: { address: ETH_ADDRESS, color: '#25292E', decimals: 18, @@ -35,9 +47,12 @@ const DEFAULT: Record = { name: 'Ethereum', symbol: 'ETH', network: Network.mainnet, - uniqueId: ETH_ADDRESS, + uniqueId: ETH_uniqueId, + networks: { + [ChainId.mainnet]: { address: ETH_ADDRESS }, + }, }, - [SOCKS_ADDRESS]: { + [SOCKS_uniqueId]: { address: SOCKS_ADDRESS, color: '#E15EE5', decimals: 18, @@ -48,9 +63,12 @@ const DEFAULT: Record = { name: 'Unisocks', symbol: 'SOCKS', network: Network.mainnet, - uniqueId: SOCKS_ADDRESS, + uniqueId: SOCKS_uniqueId, + networks: { + [ChainId.mainnet]: { address: SOCKS_ADDRESS }, + }, }, - [WBTC_ADDRESS]: { + [WBTC_uniqueId]: { address: WBTC_ADDRESS, color: '#FF9900', decimals: 8, @@ -61,7 +79,10 @@ const DEFAULT: Record = { name: 'Wrapped Bitcoin', symbol: 'WBTC', network: Network.mainnet, - uniqueId: WBTC_ADDRESS, + uniqueId: WBTC_uniqueId, + networks: { + [ChainId.mainnet]: { address: WBTC_ADDRESS }, + }, }, }; @@ -69,8 +90,8 @@ const DEFAULT: Record = { * Returns a map of the given `addresses` to their corresponding `RainbowToken` metadata. */ async function fetchMetadata(addresses: string[], chainId = ChainId.mainnet) { - const favoritesMetadata: Record = {}; - const newFavoritesMeta: Record = {}; + const favoritesMetadata: Record = {}; + const newFavoritesMeta: Record = {}; const network = ethereumUtils.getNetworkFromChainId(chainId); @@ -85,13 +106,14 @@ async function fetchMetadata(addresses: string[], chainId = ChainId.mainnet) { ); if (externalAsset) { - newFavoritesMeta[address] = { + const uniqueId = getUniqueId(externalAsset?.networks[chainId]?.address, chainId); + newFavoritesMeta[uniqueId] = { ...externalAsset, - network: ethereumUtils.getNetworkFromChainId(ChainId.mainnet), + network, address, networks: externalAsset.networks, mainnet_address: externalAsset?.networks[ChainId.mainnet]?.address, - uniqueId: getUniqueId(externalAsset?.networks[chainId]?.address, Network.mainnet), + uniqueId, isVerified: true, }; } @@ -103,22 +125,25 @@ async function fetchMetadata(addresses: string[], chainId = ChainId.mainnet) { const ethIsFavorited = addresses.includes(ETH_ADDRESS); const wethIsFavorited = addresses.includes(WETH_ADDRESS); if (newFavoritesMeta) { - if (newFavoritesMeta[WETH_ADDRESS] && ethIsFavorited) { - const favorite = newFavoritesMeta[WETH_ADDRESS]; - newFavoritesMeta[ETH_ADDRESS] = { + const WETH_uniqueId = getUniqueId(WETH_ADDRESS, ChainId.mainnet); + if (newFavoritesMeta[WETH_uniqueId] && ethIsFavorited) { + const favorite = newFavoritesMeta[WETH_uniqueId]; + const uniqueId = getUniqueId(ETH_ADDRESS, ChainId.mainnet); + newFavoritesMeta[uniqueId] = { ...favorite, address: ETH_ADDRESS, name: 'Ethereum', symbol: 'ETH', - uniqueId: getUniqueId(ETH_ADDRESS, Network.mainnet), + uniqueId, }; } - Object.entries(newFavoritesMeta).forEach(([address, favorite]) => { - if (address !== WETH_ADDRESS || wethIsFavorited) { - favoritesMetadata[address] = { ...favorite, favorite: true }; + Object.entries(newFavoritesMeta).forEach(([uniqueId, favorite]) => { + if (favorite.address !== WETH_ADDRESS || wethIsFavorited) { + favoritesMetadata[uniqueId] = { ...favorite, favorite: true }; } }); } + return favoritesMetadata; } @@ -126,9 +151,30 @@ async function fetchMetadata(addresses: string[], chainId = ChainId.mainnet) { * Refreshes the metadata associated with all favorites. */ export async function refreshFavorites() { - const favorites = Object.keys(queryClient.getQueryData(favoritesQueryKey) ?? DEFAULT); - const updatedMetadata = await fetchMetadata(favorites, ChainId.mainnet); - return updatedMetadata; + const favorites = queryClient.getQueryData>(favoritesQueryKey) ?? DEFAULT; + + const favoritesByNetwork = Object.values(favorites).reduce( + (favoritesByChain, token) => { + favoritesByChain[token.network] ??= []; + favoritesByChain[token.network].push(token.address); + return favoritesByChain; + }, + {} as Record + ); + + const updatedMetadataByNetwork = await Promise.all( + Object.entries(favoritesByNetwork).map(async ([network, networkFavorites]) => + fetchMetadata(networkFavorites, ethereumUtils.getChainIdFromNetwork(network as Network)) + ) + ); + + return updatedMetadataByNetwork.reduce( + (updatedMetadata, updatedNetworkMetadata) => ({ + ...updatedMetadata, + ...updatedNetworkMetadata, + }), + {} + ); } /** @@ -139,10 +185,11 @@ export async function refreshFavorites() { * @param chainId - The chain id of the network to toggle the favorite status of @default ChainId.mainnet */ export async function toggleFavorite(address: string, chainId = ChainId.mainnet) { - const favorites = queryClient.getQueryData>(favoritesQueryKey); - const lowercasedAddress = address.toLowerCase() as EthereumAddress; - if (Object.keys(favorites || {}).includes(lowercasedAddress)) { - queryClient.setQueryData(favoritesQueryKey, omit(favorites, lowercasedAddress)); + const favorites = queryClient.getQueryData>(favoritesQueryKey); + const lowercasedAddress = address.toLowerCase() as AddressOrEth; + const uniqueId = getUniqueId(lowercasedAddress, chainId); + if (Object.keys(favorites || {}).includes(uniqueId)) { + queryClient.setQueryData(favoritesQueryKey, omit(favorites, uniqueId)); } else { const metadata = await fetchMetadata([lowercasedAddress], chainId); queryClient.setQueryData(favoritesQueryKey, { @@ -159,11 +206,14 @@ export async function toggleFavorite(address: string, chainId = ChainId.mainnet) */ export function useFavorites(): { favorites: string[]; - favoritesMetadata: Record; + favoritesMetadata: Record; } { - const query = useQuery>(favoritesQueryKey, refreshFavorites, { - staleTime: Infinity, + const query = useQuery({ + queryKey: favoritesQueryKey, + queryFn: refreshFavorites, + staleTime: 24 * 60 * 60 * 1000, // 24hrs cacheTime: Infinity, + initialData: DEFAULT, }); const favoritesMetadata = query.data ?? {};