diff --git a/src/__swaps__/screens/Swap/Swap.tsx b/src/__swaps__/screens/Swap/Swap.tsx index f04d1e23b9f..6b1b4a72e99 100644 --- a/src/__swaps__/screens/Swap/Swap.tsx +++ b/src/__swaps__/screens/Swap/Swap.tsx @@ -28,6 +28,7 @@ import { useSwapsStore } from '@/state/swaps/swapsStore'; import { SwapWarning } from './components/SwapWarning'; import { clearCustomGasSettings } from './hooks/useCustomGas'; import { SwapProvider, useSwapContext } from './providers/swap-provider'; +import { useAccountSettings } from '@/hooks'; import { NavigateToSwapSettingsTrigger } from './components/NavigateToSwapSettingsTrigger'; /** README @@ -133,7 +134,7 @@ const useCleanupOnUnmount = () => { }; const WalletAddressObserver = () => { - const currentWalletAddress = userAssetsStore(state => state.associatedWalletAddress); + const { accountAddress } = useAccountSettings(); const { setAsset } = useSwapContext(); const setNewInputAsset = useCallback(() => { @@ -157,7 +158,7 @@ const WalletAddressObserver = () => { }, [setAsset]); useAnimatedReaction( - () => currentWalletAddress, + () => accountAddress, (current, previous) => { const didWalletAddressChange = previous && current !== previous; diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index 37c7feb897c..27a2a892de6 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -11,7 +11,7 @@ import * as i18n from '@/languages'; import { RainbowNetworkObjects } from '@/networks'; import { BASE_DEGEN_ADDRESS, DEGEN_CHAIN_DEGEN_ADDRESS, ETH_ADDRESS } from '@/references'; import { toggleFavorite } from '@/resources/favorites'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; import { ethereumUtils, haptics, showActionSheetWithOptions } from '@/utils'; import { startCase } from 'lodash'; import React, { useCallback, useMemo } from 'react'; @@ -69,7 +69,7 @@ interface OutputCoinRowProps extends PartialAsset { type CoinRowProps = InputCoinRowProps | OutputCoinRowProps; export function CoinRow({ isFavorite, onPress, output, uniqueId, testID, ...assetProps }: CoinRowProps) { - const inputAsset = userAssetsStore(state => (output ? undefined : state.getUserAsset(uniqueId))); + const inputAsset = useUserAssetsStore(state => (output ? undefined : state.getUserAsset(uniqueId))); const outputAsset = output ? (assetProps as PartialAsset) : undefined; const asset = output ? outputAsset : inputAsset; diff --git a/src/__swaps__/screens/Swap/components/SearchInput.tsx b/src/__swaps__/screens/Swap/components/SearchInput.tsx index d764efacd92..1a7badab7f1 100644 --- a/src/__swaps__/screens/Swap/components/SearchInput.tsx +++ b/src/__swaps__/screens/Swap/components/SearchInput.tsx @@ -4,7 +4,7 @@ import { opacity } from '@/__swaps__/utils/swaps'; import { Input } from '@/components/inputs'; import { Bleed, Box, Column, Columns, Text, useColorMode, useForegroundColor } from '@/design-system'; import * as i18n from '@/languages'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import React from 'react'; import Animated, { @@ -39,7 +39,7 @@ export const SearchInput = ({ const label = useForegroundColor('label'); const labelQuaternary = useForegroundColor('labelQuaternary'); - const onInputSearchQueryChange = userAssetsStore(state => state.setSearchQuery); + const onInputSearchQueryChange = useUserAssetsStore(state => state.setSearchQuery); const onOutputSearchQueryChange = useDebouncedCallback((text: string) => useSwapsStore.setState({ outputSearchQuery: text }), 100, { leading: false, diff --git a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx index 00c53e26585..8111f03e637 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx @@ -15,7 +15,7 @@ import { ContextMenuButton } from '@/components/context-menu'; import { AnimatedText, Bleed, Box, Inline, Text, TextIcon, globalColors, useColorMode } from '@/design-system'; import { useAccountAccentColor } from '@/hooks'; import { useSharedValueState } from '@/hooks/reanimated/useSharedValueState'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { showActionSheetWithOptions } from '@/utils'; import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu'; @@ -31,8 +31,11 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: const { selectedOutputChainId, setSelectedOutputChainId } = useSwapContext(); // chains sorted by balance on output, chains without balance hidden on input - const balanceSortedChainList = userAssetsStore(state => (output ? state.getBalanceSortedChainList() : state.getChainsWithBalance())); - const inputListFilter = useSharedValue(userAssetsStore.getState().filter); + const { balanceSortedChainList, filter } = useUserAssetsStore(state => ({ + balanceSortedChainList: output ? state.getBalanceSortedChainList() : state.getChainsWithBalance(), + filter: state.filter, + })); + const inputListFilter = useSharedValue(filter); const accentColor = useMemo(() => { if (c.contrast(accountColor, isDarkMode ? '#191A1C' : globalColors.white100) < (isDarkMode ? 2.125 : 1.5)) { @@ -189,7 +192,7 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: const ChainButtonIcon = ({ output }: { output: boolean | undefined }) => { const { selectedOutputChainId: animatedSelectedOutputChainId } = useSwapContext(); - const userAssetsFilter = userAssetsStore(state => (output ? undefined : state.filter)); + const userAssetsFilter = useUserAssetsStore(state => (output ? undefined : state.filter)); const selectedOutputChainId = useSharedValueState(animatedSelectedOutputChainId, { pauseSync: !output }); return ( diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx index 38e95e2475a..5b12988476b 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx @@ -8,7 +8,7 @@ import { getStandardizedUniqueIdWorklet } from '@/__swaps__/utils/swaps'; import { analyticsV2 } from '@/analytics'; import { useDelayedMount } from '@/hooks/useDelayedMount'; import * as i18n from '@/languages'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; import React, { useCallback, useMemo } from 'react'; @@ -38,7 +38,7 @@ export const TokenToSellList = () => { const TokenToSellListComponent = () => { const { inputProgress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset } = useSwapContext(); - const userAssetIds = userAssetsStore(state => state.getFilteredUserAssetIds()); + const userAssetIds = useUserAssetsStore(state => state.getFilteredUserAssetIds()); const handleSelectToken = useCallback( (token: ParsedSearchAsset | null) => { diff --git a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index 53efd2f9cc1..e78faa661c4 100644 --- a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts +++ b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import { Address } from 'viem'; import { selectUserAssetsList, @@ -9,8 +8,7 @@ import { import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; import { ParsedAssetsDictByChain, ParsedSearchAsset, UserAssetFilter } from '@/__swaps__/types/assets'; import { useAccountSettings, useDebounce } from '@/hooks'; -import { userAssetsStore } from '@/state/assets/userAssets'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; const sortBy = (by: UserAssetFilter) => { switch (by) { @@ -24,18 +22,17 @@ const sortBy = (by: UserAssetFilter) => { export const useAssetsToSell = () => { const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); - const filter = userAssetsStore(state => state.filter); - const searchQuery = userAssetsStore(state => state.inputSearchQuery); + const { filter, searchQuery } = useUserAssetsStore(state => ({ + filter: state.filter, + searchQuery: state.inputSearchQuery, + })); const debouncedAssetToSellFilter = useDebounce(searchQuery, 200); - const { connectedToHardhat } = useConnectedToHardhatStore(); - const { data: userAssets = [] } = useUserAssets( { - address: currentAddress as Address, + address: currentAddress, currency: currentCurrency, - testnetMode: connectedToHardhat, }, { select: data => diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 4740f3b61c0..7f5088d402b 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -599,7 +599,10 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { if (didSelectedAssetChange) { const assetToSet = insertUserAssetBalance - ? { ...asset, balance: (asset && userAssetsStore.getState().getUserAsset(asset.uniqueId)?.balance) || asset?.balance } + ? { + ...asset, + balance: (asset && userAssetsStore.getState().getUserAsset(asset.uniqueId)?.balance) || asset?.balance, + } : asset; if (isSameAsOtherAsset) { diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 37dc8c9f185..b3a5325b3a3 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -15,6 +15,7 @@ import { greaterThan } from '@/__swaps__/utils/numbers'; import { fetchUserAssetsByChain } from './userAssetsByChain'; import { fetchHardhatBalances, fetchHardhatBalancesByChainId } from '@/resources/assets/hardhatAssets'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const addysHttp = new RainbowFetchClient({ baseURL: 'https://addys.p.rainbow.me/v3', @@ -31,27 +32,27 @@ export const USER_ASSETS_STALE_INTERVAL = 30000; // Query Types export type UserAssetsArgs = { - address: Address; + address: Address | string; currency: SupportedCurrencyKey; testnetMode?: boolean; }; type SetUserAssetsArgs = { - address: Address; + address: Address | string; currency: SupportedCurrencyKey; userAssets?: UserAssetsResult; testnetMode?: boolean; }; type SetUserDefaultsArgs = { - address: Address; + address: Address | string; currency: SupportedCurrencyKey; staleTime: number; testnetMode?: boolean; }; type FetchUserAssetsArgs = { - address: Address; + address: Address | string; currency: SupportedCurrencyKey; testnetMode?: boolean; }; @@ -160,7 +161,7 @@ async function userAssetsQueryFunctionRetryByChain({ currency, testnetMode, }: { - address: Address; + address: Address | string; chainIds: ChainId[]; currency: SupportedCurrencyKey; testnetMode?: boolean; @@ -230,10 +231,12 @@ export async function parseUserAssets({ // Query Hook export function useUserAssets( - { address, currency, testnetMode }: UserAssetsArgs, + { address, currency }: UserAssetsArgs, config: QueryConfigWithSelect = {} ) { - return useQuery(userAssetsQueryKey({ address, currency, testnetMode }), userAssetsQueryFunction, { + const { connectedToHardhat } = useConnectedToHardhatStore(); + + return useQuery(userAssetsQueryKey({ address, currency, testnetMode: connectedToHardhat }), userAssetsQueryFunction, { ...config, refetchInterval: USER_ASSETS_REFETCH_INTERVAL, staleTime: process.env.IS_TESTING === 'true' ? 0 : 1000, diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts index b2b130aea98..864e75a5366 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts @@ -25,7 +25,7 @@ const addysHttp = new RainbowFetchClient({ // Query Types export type UserAssetsByChainArgs = { - address: Address; + address: Address | string; chainId: ChainId; currency: SupportedCurrencyKey; }; diff --git a/src/components/MobileWalletProtocolListener.tsx b/src/components/MobileWalletProtocolListener.tsx index 6db813b3b8f..27a678834cb 100644 --- a/src/components/MobileWalletProtocolListener.tsx +++ b/src/components/MobileWalletProtocolListener.tsx @@ -17,7 +17,6 @@ export const MobileWalletProtocolListener = () => { useEffect(() => { const handleMessage = async () => { if (message && lastMessageUuidRef.current !== message.uuid) { - lastMessageUuidRef.current = message.uuid; // Check if it's a handshake request diff --git a/src/components/context-menu-buttons/ChainContextMenu.tsx b/src/components/context-menu-buttons/ChainContextMenu.tsx index 5f79b99c36e..9a31a3d4911 100644 --- a/src/components/context-menu-buttons/ChainContextMenu.tsx +++ b/src/components/context-menu-buttons/ChainContextMenu.tsx @@ -4,8 +4,8 @@ import { ContextMenuButton } from '@/components/context-menu'; import { Bleed, Box, Inline, Text, TextProps } from '@/design-system'; import * as i18n from '@/languages'; import { ChainId, ChainNameDisplay } from '@/networks/types'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; import { showActionSheetWithOptions } from '@/utils'; -import { userAssetsStore } from '@/state/assets/userAssets'; import { chainNameForChainIdWithMainnetSubstitution } from '@/__swaps__/utils/chains'; interface DefaultButtonOptions { @@ -49,7 +49,7 @@ export const ChainContextMenu = ({ textWeight = 'heavy', } = defaultButtonOptions; - const balanceSortedChains = userAssetsStore(state => + const balanceSortedChains = useUserAssetsStore(state => // eslint-disable-next-line no-nested-ternary chainsToDisplay ? chainsToDisplay : excludeChainsWithNoBalance ? state.getChainsWithBalance() : state.getBalanceSortedChainList() ); diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 0dda7203474..7fb04b01e4f 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -1,10 +1,13 @@ import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; import { Address } from 'viem'; import { RainbowError, logger } from '@/logger'; -import store from '@/redux/store'; +import reduxStore, { AppState } from '@/redux/store'; import { ETH_ADDRESS, SUPPORTED_CHAIN_IDS, supportedNativeCurrencies } from '@/references'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; +import { useStore } from 'zustand'; +import { useCallback } from 'react'; import { swapsStore } from '@/state/swaps/swapsStore'; +import { useSelector } from 'react-redux'; import { ChainId } from '@/networks/types'; import { useConnectedToHardhatStore } from '../connectedToHardhat'; @@ -26,7 +29,6 @@ const getDefaultCacheKeys = (): Set => { const CACHE_ITEMS_TO_PRESERVE = getDefaultCacheKeys(); export interface UserAssetsState { - associatedWalletAddress: Address | undefined; chainBalances: Map; currentAbortController: AbortController; filter: UserAssetFilter; @@ -44,7 +46,7 @@ export interface UserAssetsState { selectUserAssets: (selector: (asset: ParsedSearchAsset) => boolean) => Generator<[UniqueId, ParsedSearchAsset], void, unknown>; setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => void; setSearchQuery: (query: string) => void; - setUserAssets: (associatedWalletAddress: Address, userAssets: Map | ParsedSearchAsset[]) => void; + setUserAssets: (userAssets: Map | ParsedSearchAsset[]) => void; } // NOTE: We are serializing Map as an Array<[UniqueId, ParsedSearchAsset]> @@ -122,227 +124,257 @@ function deserializeUserAssetsState(serializedState: string) { }; } -export const userAssetsStore = createRainbowStore( - (set, get) => ({ - associatedWalletAddress: undefined, - chainBalances: new Map(), - currentAbortController: new AbortController(), - filter: 'all', - idsByChain: new Map(), - inputSearchQuery: '', - searchCache: new Map(), - userAssets: new Map(), - - getBalanceSortedChainList: () => { - const chainBalances = [...get().chainBalances.entries()]; - chainBalances.sort(([, balanceA], [, balanceB]) => balanceB - balanceA); - return chainBalances.map(([chainId]) => chainId); - }, - - getChainsWithBalance: () => { - const chainBalances = [...get().chainBalances.entries()]; - const chainsWithBalances = chainBalances.filter(([, balance]) => !!balance); - return chainsWithBalances.map(([chainId]) => chainId); - }, - - getFilteredUserAssetIds: () => { - const { filter, inputSearchQuery: rawSearchQuery, selectUserAssetIds, setSearchCache } = get(); - - const smallBalanceThreshold = supportedNativeCurrencies[store.getState().settings.nativeCurrency].userAssetsSmallThreshold; - - const inputSearchQuery = rawSearchQuery.trim().toLowerCase(); - const queryKey = getSearchQueryKey({ filter, searchQuery: inputSearchQuery }); - - // Use an external function to get the cache to prevent updates in response to changes in the cache - const cachedData = getCurrentCache().get(queryKey); - - // Check if the search results are already cached - if (cachedData) { - return cachedData; - } else { - const chainIdFilter = filter === 'all' ? null : filter; - const searchRegex = inputSearchQuery.length > 0 ? new RegExp(escapeRegExp(inputSearchQuery), 'i') : null; - - const filteredIds = Array.from( - selectUserAssetIds( - asset => - (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold && - (!chainIdFilter || asset.chainId === chainIdFilter) && - (!searchRegex || - searchRegex.test(asset.name) || - searchRegex.test(asset.symbol) || - asset.address.toLowerCase() === inputSearchQuery), - filter - ) - ); - - setSearchCache(queryKey, filteredIds); - - return filteredIds; - } - }, - getHighestValueEth: () => { - const preferredNetwork = swapsStore.getState().preferredNetwork; - const assets = get().userAssets; +export const createUserAssetsStore = (address: Address | string) => + createRainbowStore( + (set, get) => ({ + chainBalances: new Map(), + currentAbortController: new AbortController(), + filter: 'all', + idsByChain: new Map(), + inputSearchQuery: '', + searchCache: new Map(), + userAssets: new Map(), + + getBalanceSortedChainList: () => { + const chainBalances = [...get().chainBalances.entries()]; + chainBalances.sort(([, balanceA], [, balanceB]) => balanceB - balanceA); + return chainBalances.map(([chainId]) => chainId); + }, + + getChainsWithBalance: () => { + const chainBalances = [...get().chainBalances.entries()]; + const chainsWithBalances = chainBalances.filter(([, balance]) => !!balance); + return chainsWithBalances.map(([chainId]) => chainId); + }, + + getFilteredUserAssetIds: () => { + const { filter, inputSearchQuery: rawSearchQuery, selectUserAssetIds, setSearchCache } = get(); + + const smallBalanceThreshold = supportedNativeCurrencies[reduxStore.getState().settings.nativeCurrency].userAssetsSmallThreshold; + + const inputSearchQuery = rawSearchQuery.trim().toLowerCase(); + const queryKey = getSearchQueryKey({ filter, searchQuery: inputSearchQuery }); + + // Use an external function to get the cache to prevent updates in response to changes in the cache + const cachedData = getCurrentSearchCache()?.get(queryKey); + + // Check if the search results are already cached + if (cachedData) { + return cachedData; + } else { + const chainIdFilter = filter === 'all' ? null : filter; + const searchRegex = inputSearchQuery.length > 0 ? new RegExp(escapeRegExp(inputSearchQuery), 'i') : null; + + const filteredIds = Array.from( + selectUserAssetIds( + asset => + (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold && + (!chainIdFilter || asset.chainId === chainIdFilter) && + (!searchRegex || + searchRegex.test(asset.name) || + searchRegex.test(asset.symbol) || + asset.address.toLowerCase() === inputSearchQuery), + filter + ) + ); + + setSearchCache(queryKey, filteredIds); + + return filteredIds; + } + }, + getHighestValueEth: () => { + const preferredNetwork = swapsStore.getState().preferredNetwork; + const assets = get().userAssets; - let highestValueEth = null; + let highestValueEth = null; - for (const [, asset] of assets) { - if (asset.mainnetAddress !== ETH_ADDRESS) continue; + for (const [, asset] of assets) { + if (asset.mainnetAddress !== ETH_ADDRESS) continue; - if (preferredNetwork && asset.chainId === preferredNetwork) { - return asset; - } + if (preferredNetwork && asset.chainId === preferredNetwork) { + return asset; + } - if (!highestValueEth || asset.balance > highestValueEth.balance) { - highestValueEth = asset; + if (!highestValueEth || asset.balance > highestValueEth.balance) { + highestValueEth = asset; + } } - } - return highestValueEth; - }, + return highestValueEth; + }, - getUserAsset: (uniqueId: UniqueId) => get().userAssets.get(uniqueId) || null, + getUserAsset: (uniqueId: UniqueId) => get().userAssets.get(uniqueId) || null, - getUserAssets: () => Array.from(get().userAssets.values()) || [], + getUserAssets: () => Array.from(get().userAssets.values()) || [], - selectUserAssetIds: function* (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) { - const { currentAbortController, idsByChain, userAssets } = get(); + selectUserAssetIds: function* (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) { + const { currentAbortController, idsByChain, userAssets } = get(); - const assetIds = filter ? idsByChain.get(filter) || [] : idsByChain.get('all') || []; + const assetIds = filter ? idsByChain.get(filter) || [] : idsByChain.get('all') || []; - for (const id of assetIds) { - if (currentAbortController?.signal.aborted) { - return; - } - const asset = userAssets.get(id); - if (asset && selector(asset)) { - yield id; + for (const id of assetIds) { + if (currentAbortController?.signal.aborted) { + return; + } + const asset = userAssets.get(id); + if (asset && selector(asset)) { + yield id; + } } - } - }, + }, - selectUserAssets: function* (selector: (asset: ParsedSearchAsset) => boolean) { - const { currentAbortController, userAssets } = get(); + selectUserAssets: function* (selector: (asset: ParsedSearchAsset) => boolean) { + const { currentAbortController, userAssets } = get(); - for (const [id, asset] of userAssets) { - if (currentAbortController?.signal.aborted) { - return; - } - if (selector(asset)) { - yield [id, asset]; + for (const [id, asset] of userAssets) { + if (currentAbortController?.signal.aborted) { + return; + } + if (selector(asset)) { + yield [id, asset]; + } } - } - }, + }, - setSearchQuery: query => - set(state => { - const { currentAbortController } = state; + setSearchQuery: query => + set(state => { + const { currentAbortController } = state; - // Abort any ongoing search work - currentAbortController.abort(); + // Abort any ongoing search work + currentAbortController.abort(); - // Create a new AbortController for the new query - const abortController = new AbortController(); + // Create a new AbortController for the new query + const abortController = new AbortController(); - return { inputSearchQuery: query.trim().toLowerCase(), currentAbortController: abortController }; - }), + return { inputSearchQuery: query.trim().toLowerCase(), currentAbortController: abortController }; + }), - setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => { - set(state => { - const newCache = new Map(state.searchCache).set(queryKey, filteredIds); + setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => { + set(state => { + const newCache = new Map(state.searchCache).set(queryKey, filteredIds); - // Prune the cache if it exceeds the maximum size - if (newCache.size > SEARCH_CACHE_MAX_ENTRIES) { - // Get the oldest key that isn't a key to preserve - for (const key of newCache.keys()) { - if (!CACHE_ITEMS_TO_PRESERVE.has(key)) { - newCache.delete(key); - break; + // Prune the cache if it exceeds the maximum size + if (newCache.size > SEARCH_CACHE_MAX_ENTRIES) { + // Get the oldest key that isn't a key to preserve + for (const key of newCache.keys()) { + if (!CACHE_ITEMS_TO_PRESERVE.has(key)) { + newCache.delete(key); + break; + } } } - } - - return { searchCache: newCache }; - }); - }, - setUserAssets: (associatedWalletAddress: Address, userAssets: Map | ParsedSearchAsset[]) => - set(() => { - const idsByChain = new Map(); - const unsortedChainBalances = new Map(); - - userAssets.forEach(asset => { - const balance = Number(asset.native.balance.amount) ?? 0; - unsortedChainBalances.set(asset.chainId, (unsortedChainBalances.get(asset.chainId) || 0) + balance); - idsByChain.set(asset.chainId, (idsByChain.get(asset.chainId) || []).concat(asset.uniqueId)); + return { searchCache: newCache }; }); + }, + + setUserAssets: (userAssets: Map | ParsedSearchAsset[]) => + set(() => { + const idsByChain = new Map(); + const unsortedChainBalances = new Map(); + + userAssets.forEach(asset => { + const balance = Number(asset.native.balance.amount) ?? 0; + unsortedChainBalances.set(asset.chainId, (unsortedChainBalances.get(asset.chainId) || 0) + balance); + idsByChain.set(asset.chainId, (idsByChain.get(asset.chainId) || []).concat(asset.uniqueId)); + }); + + // Ensure all supported chains are in the map with a fallback value of 0 + SUPPORTED_CHAIN_IDS({ testnetMode: useConnectedToHardhatStore.getState().connectedToHardhat }).forEach(chainId => { + if (!unsortedChainBalances.has(chainId)) { + unsortedChainBalances.set(chainId, 0); + idsByChain.set(chainId, []); + } + }); - // Ensure all supported chains are in the map with a fallback value of 0 - SUPPORTED_CHAIN_IDS({ testnetMode: useConnectedToHardhatStore.getState().connectedToHardhat }).forEach(chainId => { - if (!unsortedChainBalances.has(chainId)) { - unsortedChainBalances.set(chainId, 0); - idsByChain.set(chainId, []); - } - }); + // Sort the existing map by balance in descending order + const sortedEntries = Array.from(unsortedChainBalances.entries()).sort(([, balanceA], [, balanceB]) => balanceB - balanceA); + const chainBalances = new Map(); - // Sort the existing map by balance in descending order - const sortedEntries = Array.from(unsortedChainBalances.entries()).sort(([, balanceA], [, balanceB]) => balanceB - balanceA); - const chainBalances = new Map(); + sortedEntries.forEach(([chainId, balance]) => { + chainBalances.set(chainId, balance); + idsByChain.set(chainId, idsByChain.get(chainId) || []); + }); - sortedEntries.forEach(([chainId, balance]) => { - chainBalances.set(chainId, balance); - idsByChain.set(chainId, idsByChain.get(chainId) || []); - }); + const isMap = userAssets instanceof Map; + const allIdsArray = isMap ? Array.from(userAssets.keys()) : userAssets.map(asset => asset.uniqueId); + const userAssetsMap = isMap ? userAssets : new Map(userAssets.map(asset => [asset.uniqueId, asset])); - const isMap = userAssets instanceof Map; - const allIdsArray = isMap ? Array.from(userAssets.keys()) : userAssets.map(asset => asset.uniqueId); - const userAssetsMap = isMap ? userAssets : new Map(userAssets.map(asset => [asset.uniqueId, asset])); + idsByChain.set('all', allIdsArray); - idsByChain.set('all', allIdsArray); + const smallBalanceThreshold = supportedNativeCurrencies[reduxStore.getState().settings.nativeCurrency].userAssetsSmallThreshold; - const smallBalanceThreshold = supportedNativeCurrencies[store.getState().settings.nativeCurrency].userAssetsSmallThreshold; + const filteredAllIdsArray = allIdsArray.filter(id => { + const asset = userAssetsMap.get(id); + return asset && (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold; + }); - const filteredAllIdsArray = allIdsArray.filter(id => { - const asset = userAssetsMap.get(id); - return asset && (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold; - }); + const searchCache = new Map(); - const searchCache = new Map(); + Array.from(chainBalances.keys()).forEach(userAssetFilter => { + const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); + searchCache.set(`${userAssetFilter}`, filteredIds); + }); - Array.from(chainBalances.keys()).forEach(userAssetFilter => { - const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); - searchCache.set(`${userAssetFilter}`, filteredIds); - }); + searchCache.set('all', filteredAllIdsArray); - searchCache.set('all', filteredAllIdsArray); - - if (isMap) { - return { associatedWalletAddress, chainBalances, idsByChain, searchCache, userAssets }; - } else - return { - associatedWalletAddress, - chainBalances, - idsByChain, - searchCache, - userAssets: userAssetsMap, - }; - }), - }), - { - deserializer: deserializeUserAssetsState, - partialize: state => ({ - associatedWalletAddress: state.associatedWalletAddress, - chainBalances: state.chainBalances, - idsByChain: state.idsByChain, - userAssets: state.userAssets, + if (isMap) { + return { chainBalances, idsByChain, searchCache, userAssets }; + } else + return { + chainBalances, + idsByChain, + searchCache, + userAssets: userAssetsMap, + }; + }), }), - serializer: serializeUserAssetsState, - storageKey: 'userAssets', - version: 3, + { + storageKey: `userAssets_${address}`, + version: 0, + serializer: serializeUserAssetsState, + deserializer: deserializeUserAssetsState, + } + ); + +type UserAssetsStoreType = ReturnType; + +interface StoreManagerState { + stores: Map
; +} + +const storeManager = createRainbowStore(() => ({ + stores: new Map(), +})); + +function getOrCreateStore(address?: Address | string): UserAssetsStoreType { + const accountAddress = address ?? reduxStore.getState().settings.accountAddress; + const { stores } = storeManager.getState(); + let store = stores.get(accountAddress); + + if (!store) { + store = createUserAssetsStore(accountAddress); + storeManager.setState(state => ({ + stores: new Map(state.stores).set(accountAddress, store as UserAssetsStoreType), + })); } -); -function getCurrentCache(): Map { - return userAssetsStore.getState().searchCache; + return store; +} + +export const userAssetsStore = { + getState: () => getOrCreateStore().getState(), + setState: (partial: Partial | ((state: UserAssetsState) => Partial)) => + getOrCreateStore().setState(partial), +}; + +export function useUserAssetsStore(selector: (state: UserAssetsState) => T) { + const address = useSelector((state: AppState) => state.settings.accountAddress); + const store = getOrCreateStore(address); + return useStore(store, useCallback(selector, [])); +} + +function getCurrentSearchCache(): Map | undefined { + return getOrCreateStore().getState().searchCache; } diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index 4c8c379c775..139d4ddb547 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -1,4 +1,3 @@ -import { Address } from 'viem'; import { useAccountSettings } from '@/hooks'; import { userAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; @@ -6,31 +5,27 @@ import { selectUserAssetsList, selectorFilterByUserChains } from '@/__swaps__/sc import { ParsedSearchAsset } from '@/__swaps__/types/assets'; import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; import { ChainId } from '@/networks/types'; -import { useConnectedToHardhatStore } from '../connectedToHardhat'; export const UserAssetsSync = function UserAssetsSync() { - const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); + const { accountAddress, nativeCurrency: currentCurrency } = useAccountSettings(); - const userAssetsWalletAddress = userAssetsStore(state => state.associatedWalletAddress); const isSwapsOpen = useSwapsStore(state => state.isSwapsOpen); - const { connectedToHardhat } = useConnectedToHardhatStore(); useUserAssets( { - address: currentAddress as Address, + address: accountAddress, currency: currentCurrency, - testnetMode: connectedToHardhat, }, { - enabled: !isSwapsOpen || userAssetsWalletAddress !== currentAddress, + enabled: !isSwapsOpen, select: data => selectorFilterByUserChains({ data, selector: selectUserAssetsList, }), onSuccess: data => { - if (!isSwapsOpen || userAssetsWalletAddress !== currentAddress) { - userAssetsStore.getState().setUserAssets(currentAddress as Address, data as ParsedSearchAsset[]); + if (!isSwapsOpen) { + userAssetsStore.getState().setUserAssets(data as ParsedSearchAsset[]); const inputAsset = userAssetsStore.getState().getHighestValueEth(); useSwapsStore.setState({