From 551eb152b6b2d42d247c97212f7619347ece822d Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Mon, 19 Aug 2024 17:43:22 -0400 Subject: [PATCH 01/13] done --- src/__swaps__/screens/Swap/Swap.tsx | 84 ++-- .../screens/Swap/components/CoinRow.tsx | 9 +- .../screens/Swap/components/SearchInput.tsx | 10 +- .../components/TokenList/ChainSelection.tsx | 20 +- .../components/TokenList/TokenToBuyList.tsx | 31 +- .../components/TokenList/TokenToSellList.tsx | 35 +- .../screens/Swap/hooks/useAssetsToSell.ts | 8 +- .../screens/Swap/providers/swap-provider.tsx | 25 +- src/__swaps__/utils/swaps.ts | 11 +- .../control-panel/ControlPanel.tsx | 16 +- .../ProfileActionButtonsRow.tsx | 7 +- .../context-menu-buttons/ChainContextMenu.tsx | 7 +- .../expanded-state/AvailableNetworksv2.tsx | 16 +- .../sheet-action-buttons/SwapActionButton.tsx | 18 +- src/state/assets/userAssets.ts | 401 +++++++++--------- src/state/sync/UserAssetsSync.tsx | 19 +- 16 files changed, 409 insertions(+), 308 deletions(-) diff --git a/src/__swaps__/screens/Swap/Swap.tsx b/src/__swaps__/screens/Swap/Swap.tsx index 6634ee6b99b..b9998a8327f 100644 --- a/src/__swaps__/screens/Swap/Swap.tsx +++ b/src/__swaps__/screens/Swap/Swap.tsx @@ -23,11 +23,14 @@ import { SwapAssetType } from '@/__swaps__/types/swap'; import { parseSearchAsset } from '@/__swaps__/utils/assets'; import { AbsolutePortalRoot } from '@/components/AbsolutePortal'; import { useDelayedMount } from '@/hooks/useDelayedMount'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import { SwapWarning } from './components/SwapWarning'; import { clearCustomGasSettings } from './hooks/useCustomGas'; import { SwapProvider, useSwapContext } from './providers/swap-provider'; +import store from '@/redux/store'; +import { Address } from 'viem'; +import { useAccountSettings } from '@/hooks'; /** README * This prototype is largely driven by Reanimated and Gesture Handler, which @@ -103,59 +106,68 @@ const useMountSignal = () => { }; const useCleanupOnUnmount = () => { + const { accountAddress } = useAccountSettings(); + useEffect(() => { return () => { - const highestValueEth = userAssetsStore.getState().getHighestValueEth(); - const parsedAsset = highestValueEth - ? parseSearchAsset({ - assetWithPrice: undefined, - searchAsset: highestValueEth, - userAsset: highestValueEth, - }) - : null; - - useSwapsStore.setState({ - inputAsset: parsedAsset, - isSwapsOpen: false, - outputAsset: null, - outputSearchQuery: '', - quote: null, - selectedOutputChainId: parsedAsset?.chainId ?? ChainId.mainnet, - }); - - userAssetsStore.setState({ filter: 'all', inputSearchQuery: '' }); + const userAssetsStore = getUserAssetsStore(accountAddress as Address); + + if (userAssetsStore) { + const highestValueEth = userAssetsStore.getState().getHighestValueEth(); + const parsedAsset = highestValueEth + ? parseSearchAsset({ + assetWithPrice: undefined, + searchAsset: highestValueEth, + userAsset: highestValueEth, + }) + : null; + + useSwapsStore.setState({ + inputAsset: parsedAsset, + isSwapsOpen: false, + outputAsset: null, + outputSearchQuery: '', + quote: null, + selectedOutputChainId: parsedAsset?.chainId ?? ChainId.mainnet, + }); + + userAssetsStore.setState({ filter: 'all', inputSearchQuery: '' }); + } clearCustomGasSettings(); }; - }, []); + }, [accountAddress]); }; const WalletAddressObserver = () => { - const currentWalletAddress = userAssetsStore(state => state.associatedWalletAddress); + const { accountAddress } = useAccountSettings(); const { setAsset } = useSwapContext(); const setNewInputAsset = useCallback(() => { - const newHighestValueEth = userAssetsStore.getState().getHighestValueEth(); + const userAssetsStore = getUserAssetsStore(accountAddress as Address); + if (userAssetsStore) { + const newHighestValueEth = userAssetsStore.getState().getHighestValueEth(); - if (userAssetsStore.getState().filter !== 'all') { - userAssetsStore.setState({ filter: 'all' }); - } - - setAsset({ - type: SwapAssetType.inputAsset, - asset: newHighestValueEth, - }); + if (userAssetsStore.getState().filter !== 'all') { + userAssetsStore.setState({ filter: 'all' }); + } - if (userAssetsStore.getState().userAssets.size === 0) { setAsset({ - type: SwapAssetType.outputAsset, - asset: null, + type: SwapAssetType.inputAsset, + asset: newHighestValueEth, }); + + if (userAssetsStore.getState().userAssets.size === 0) { + setAsset({ + type: SwapAssetType.outputAsset, + asset: null, + }); + } } - }, [setAsset]); + }, [accountAddress, 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 2c2bcc372ca..38c8322b3ba 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -11,13 +11,14 @@ import * as i18n from '@/languages'; import { RainbowNetworks } 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'; import { GestureResponderEvent } from 'react-native'; import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu'; import { SwapCoinIcon } from './SwapCoinIcon'; +import { Address } from 'viem'; export const COIN_ROW_WITH_PADDING_HEIGHT = 56; @@ -52,6 +53,7 @@ interface InputCoinRowProps { onPress: (asset: ParsedSearchAsset | null) => void; output?: false | undefined; uniqueId: string; + walletAddress: string; } type PartialAsset = Pick; @@ -62,12 +64,13 @@ interface OutputCoinRowProps extends PartialAsset { output: true; nativePriceChange?: string; isTrending?: boolean; + walletAddress: string; } type CoinRowProps = InputCoinRowProps | OutputCoinRowProps; -export function CoinRow({ isFavorite, onPress, output, uniqueId, ...assetProps }: CoinRowProps) { - const inputAsset = userAssetsStore(state => (output ? undefined : state.getUserAsset(uniqueId))); +export function CoinRow({ isFavorite, onPress, output, uniqueId, walletAddress, ...assetProps }: CoinRowProps) { + const inputAsset = useUserAssetsStore(walletAddress as Address)(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 8b215bf64f7..1e37c17888f 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 { getUserAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import React from 'react'; import Animated, { @@ -17,6 +17,8 @@ import Animated, { } from 'react-native-reanimated'; import { useDebouncedCallback } from 'use-debounce'; import { SearchInputButton } from './SearchInputButton'; +import { useAccountSettings } from '@/hooks'; +import { Address } from 'viem'; const AnimatedInput = Animated.createAnimatedComponent(Input); @@ -34,12 +36,13 @@ export const SearchInput = ({ }) => { const { isDarkMode } = useColorMode(); const { inputProgress, inputSearchRef, outputProgress, outputSearchRef } = useSwapContext(); + const { accountAddress } = useAccountSettings(); const fillTertiary = useForegroundColor('fillTertiary'); const label = useForegroundColor('label'); const labelQuaternary = useForegroundColor('labelQuaternary'); - const onInputSearchQueryChange = userAssetsStore(state => state.setSearchQuery); + const onInputSearchQueryChange = useUserAssetsStore(accountAddress as Address)(state => state.setSearchQuery); const onOutputSearchQueryChange = useDebouncedCallback((text: string) => useSwapsStore.setState({ outputSearchQuery: text }), 100, { leading: false, @@ -108,7 +111,8 @@ export const SearchInput = ({ useSwapsStore.setState({ outputSearchQuery: '' }); } } else { - if (userAssetsStore.getState().inputSearchQuery !== '') { + const userAssetsStore = getUserAssetsStore(accountAddress as Address); + if (userAssetsStore && userAssetsStore.getState().inputSearchQuery !== '') { userAssetsStore.getState().setSearchQuery(''); } } diff --git a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx index 8700f9408a1..88c2aaaeeb9 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx @@ -13,12 +13,13 @@ import { analyticsV2 } from '@/analytics'; import { ChainImage } from '@/components/coin-icon/ChainImage'; import { ContextMenuButton } from '@/components/context-menu'; import { AnimatedText, Bleed, Box, Inline, Text, TextIcon, globalColors, useColorMode } from '@/design-system'; -import { useAccountAccentColor } from '@/hooks'; +import { useAccountAccentColor, useAccountSettings } from '@/hooks'; import { useSharedValueState } from '@/hooks/reanimated/useSharedValueState'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { ethereumUtils, showActionSheetWithOptions } from '@/utils'; import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu'; +import { Address } from 'viem'; type ChainSelectionProps = { allText?: string; @@ -28,11 +29,15 @@ type ChainSelectionProps = { export const ChainSelection = memo(function ChainSelection({ allText, output }: ChainSelectionProps) { const { isDarkMode } = useColorMode(); const { accentColor: accountColor } = useAccountAccentColor(); + const { accountAddress } = useAccountSettings(); 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(accountAddress as Address)(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)) { @@ -63,12 +68,12 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: setSelectedOutputChainId(Number(actionKey) as ChainId); } else { inputListFilter.value = actionKey === 'all' ? 'all' : (Number(actionKey) as ChainId); - userAssetsStore.setState({ + getUserAssetsStore(accountAddress as Address)?.setState({ filter: actionKey === 'all' ? 'all' : (Number(actionKey) as ChainId), }); } }, - [inputListFilter, output, setSelectedOutputChainId] + [accountAddress, inputListFilter, output, setSelectedOutputChainId] ); const menuConfig = useMemo(() => { @@ -188,8 +193,9 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: const ChainButtonIcon = ({ output }: { output: boolean | undefined }) => { const { selectedOutputChainId: animatedSelectedOutputChainId } = useSwapContext(); + const { accountAddress } = useAccountSettings(); - const userAssetsFilter = userAssetsStore(state => (output ? undefined : state.filter)); + const userAssetsFilter = useUserAssetsStore(accountAddress as Address)(state => (output ? undefined : state.filter)); const selectedOutputChainId = useSharedValueState(animatedSelectedOutputChainId, { pauseSync: !output }); return ( diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx index ee486ec0fe9..d6cf27a0a06 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx @@ -13,7 +13,7 @@ import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; import { Box, Inline, Stack, Text, TextIcon, useColorMode } from '@/design-system'; import { palettes } from '@/design-system/color/palettes'; import * as i18n from '@/languages'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; import { FlashList } from '@shopify/flash-list'; @@ -22,6 +22,8 @@ import { ScrollViewProps } from 'react-native'; import Animated, { runOnUI, useAnimatedProps, useAnimatedStyle, withTiming } from 'react-native-reanimated'; import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '../../constants'; import { ChainSelection } from './ChainSelection'; +import { useAccountSettings } from '@/hooks'; +import { Address } from 'viem'; export const BUY_LIST_HEADER_HEIGHT = 20 + 10 + 8; // paddingTop + height + paddingBottom @@ -86,6 +88,7 @@ const ScrollViewWithRef = forwardRef(function ScrollViewWit export const TokenToBuyList = () => { const { internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, outputProgress, setAsset } = useSwapContext(); const { results: sections, isLoading } = useSearchCurrencyLists(); + const { accountAddress } = useAccountSettings(); const handleSelectToken = useCallback( (token: SearchAsset) => { @@ -99,17 +102,20 @@ export const TokenToBuyList = () => { } })(); - const userAsset = userAssetsStore.getState().getUserAsset(token.uniqueId); - const parsedAsset = parseSearchAsset({ - assetWithPrice: undefined, - searchAsset: token, - userAsset: userAsset ?? undefined, - }); + const userAssetsStore = getUserAssetsStore(accountAddress as Address); + if (userAssetsStore) { + const userAsset = userAssetsStore.getState().getUserAsset(token.uniqueId); + const parsedAsset = parseSearchAsset({ + assetWithPrice: undefined, + searchAsset: token, + userAsset: userAsset ?? undefined, + }); - setAsset({ - type: SwapAssetType.outputAsset, - asset: parsedAsset, - }); + setAsset({ + type: SwapAssetType.outputAsset, + asset: parsedAsset, + }); + } const { outputSearchQuery } = swapsStore.getState(); @@ -121,7 +127,7 @@ export const TokenToBuyList = () => { }); } }, - [internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset] + [accountAddress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset] ); const animatedListPadding = useAnimatedStyle(() => { @@ -172,6 +178,7 @@ export const TokenToBuyList = () => { output symbol={item.symbol} uniqueId={item.uniqueId} + walletAddress={accountAddress} /> ); }} diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx index f28acfd8dfb..fdb5c40c7ea 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx @@ -7,7 +7,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 { getUserAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; import { FlashList } from '@shopify/flash-list'; @@ -15,6 +15,8 @@ import React, { useCallback, useMemo } from 'react'; import Animated, { runOnUI, useAnimatedProps, useAnimatedStyle } from 'react-native-reanimated'; import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '../../constants'; import { ChainSelection } from './ChainSelection'; +import { useAccountSettings } from '@/hooks'; +import { Address } from 'viem'; export const SELL_LIST_HEADER_HEIGHT = 20 + 10 + 14; // paddingTop + height + paddingBottom @@ -31,8 +33,9 @@ export const TokenToSellList = () => { const TokenToSellListComponent = () => { const { inputProgress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset } = useSwapContext(); + const { accountAddress } = useAccountSettings(); - const userAssetIds = userAssetsStore(state => state.getFilteredUserAssetIds()); + const userAssetIds = useUserAssetsStore(accountAddress as Address)(state => state.getFilteredUserAssetIds()); const handleSelectToken = useCallback( (token: ParsedSearchAsset | null) => { @@ -53,17 +56,20 @@ const TokenToSellListComponent = () => { asset: token, }); - const { inputSearchQuery } = userAssetsStore.getState(); + const userAssetsStore = getUserAssetsStore(accountAddress as Address); + if (userAssetsStore) { + const { inputSearchQuery } = userAssetsStore.getState(); - // track what search query the user had prior to selecting an asset - if (inputSearchQuery.trim().length) { - analyticsV2.track(analyticsV2.event.swapsSearchedForToken, { - query: inputSearchQuery, - type: 'input', - }); + // track what search query the user had prior to selecting an asset + if (inputSearchQuery.trim().length) { + analyticsV2.track(analyticsV2.event.swapsSearchedForToken, { + query: inputSearchQuery, + type: 'input', + }); + } } }, - [internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset] + [accountAddress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset] ); const animatedListPadding = useAnimatedStyle(() => { @@ -92,7 +98,14 @@ const TokenToSellListComponent = () => { estimatedListSize={{ height: EXPANDED_INPUT_HEIGHT - 77, width: DEVICE_WIDTH - 24 }} keyExtractor={uniqueId => uniqueId} renderItem={({ item: uniqueId }) => { - return handleSelectToken(asset)} output={false} uniqueId={uniqueId} />; + return ( + handleSelectToken(asset)} + output={false} + uniqueId={uniqueId} + walletAddress={accountAddress} + /> + ); }} renderScrollComponent={props => { return ( diff --git a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index 13f6d54e02d..2070496e624 100644 --- a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts +++ b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts @@ -9,7 +9,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 { useUserAssetsStore } from '@/state/assets/userAssets'; const sortBy = (by: UserAssetFilter) => { switch (by) { @@ -23,8 +23,10 @@ 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(currentAddress as Address)(state => ({ + filter: state.filter, + searchQuery: state.inputSearchQuery, + })); const debouncedAssetToSellFilter = useDebounce(searchQuery, 200); diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 6368bc161b9..4367e1e663f 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -43,7 +43,7 @@ import { walletExecuteRap } from '@/raps/execute'; import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; import { queryClient } from '@/react-query'; import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { ethereumUtils, haptics } from '@/utils'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; @@ -131,7 +131,7 @@ interface SwapProviderProps { } export const SwapProvider = ({ children }: SwapProviderProps) => { - const { nativeCurrency } = useAccountSettings(); + const { accountAddress, nativeCurrency } = useAccountSettings(); const isFetching = useSharedValue(false); const isQuoteStale = useSharedValue(0); // TODO: Convert this to a boolean @@ -146,8 +146,8 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const lastTypedInput = useSharedValue('inputAmount'); const focusedInput = useSharedValue('inputAmount'); - const initialSelectedInputAsset = parseAssetAndExtend({ asset: swapsStore.getState().inputAsset }); - const initialSelectedOutputAsset = parseAssetAndExtend({ asset: swapsStore.getState().outputAsset }); + const initialSelectedInputAsset = parseAssetAndExtend({ asset: swapsStore.getState().inputAsset, walletAddress: accountAddress }); + const initialSelectedOutputAsset = parseAssetAndExtend({ asset: swapsStore.getState().outputAsset, walletAddress: accountAddress }); const internalSelectedInputAsset = useSharedValue(initialSelectedInputAsset); const internalSelectedOutputAsset = useSharedValue(initialSelectedOutputAsset); @@ -551,7 +551,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const setAsset = useCallback( ({ type, asset }: { type: SwapAssetType; asset: ParsedSearchAsset | null }) => { const insertUserAssetBalance = type !== SwapAssetType.inputAsset; - const extendedAsset = parseAssetAndExtend({ asset, insertUserAssetBalance }); + const extendedAsset = parseAssetAndExtend({ asset, insertUserAssetBalance, walletAddress: accountAddress }); const otherSelectedAsset = type === SwapAssetType.inputAsset ? internalSelectedOutputAsset.value : internalSelectedInputAsset.value; const isSameAsOtherAsset = !!(otherSelectedAsset && otherSelectedAsset.uniqueId === extendedAsset?.uniqueId); @@ -595,7 +595,15 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { if (didSelectedAssetChange) { const assetToSet = insertUserAssetBalance - ? { ...asset, balance: (asset && userAssetsStore.getState().getUserAsset(asset.uniqueId)?.balance) || asset?.balance } + ? { + ...asset, + balance: + (asset && + getUserAssetsStore(accountAddress as Address) + ?.getState() + .getUserAsset(asset.uniqueId)?.balance) || + asset?.balance, + } : asset; if (isSameAsOtherAsset) { @@ -646,9 +654,10 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }, [ SwapInputController.quoteFetchingInterval, + accountAddress, handleProgressNavigation, - internalSelectedInputAsset, - internalSelectedOutputAsset, + internalSelectedInputAsset.value, + internalSelectedOutputAsset.value, selectedOutputChainId, updateAssetValue, ] diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index 33071792138..bc7d0047890 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -19,7 +19,7 @@ import * as i18n from '@/languages'; import { RainbowConfig } from '@/model/remoteConfig'; import store from '@/redux/store'; import { ETH_ADDRESS } from '@/references'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore } from '@/state/assets/userAssets'; import { colors } from '@/styles'; import { BigNumberish } from '@ethersproject/bignumber'; import { CrosschainQuote, ETH_ADDRESS as ETH_ADDRESS_AGGREGATOR, Quote, QuoteParams, SwapType, WRAPPED_ASSET } from '@rainbow-me/swaps'; @@ -40,6 +40,7 @@ import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from import { inputKeys } from '../types/swap'; import { valueBasedDecimalFormatter } from './decimalFormatter'; import { convertAmountToRawAmount } from './numbers'; +import { Address } from 'viem'; // /---- 🎨 Color functions 🎨 ----/ // // @@ -569,6 +570,7 @@ export const priceForAsset = ({ type ParseAssetAndExtendProps = { asset: ParsedSearchAsset | null; insertUserAssetBalance?: boolean; + walletAddress: string; }; const ETH_COLORS: Colors = { @@ -585,6 +587,7 @@ export const getStandardizedUniqueIdWorklet = ({ address, chainId }: { address: export const parseAssetAndExtend = ({ asset, insertUserAssetBalance, + walletAddress, }: ParseAssetAndExtendProps): ExtendedAnimatedAssetWithColors | null => { if (!asset) { return null; @@ -596,7 +599,11 @@ export const parseAssetAndExtend = ({ }); const uniqueId = getStandardizedUniqueIdWorklet({ address: asset.address, chainId: asset.chainId }); - const balance = insertUserAssetBalance ? userAssetsStore.getState().getUserAsset(uniqueId)?.balance || asset.balance : asset.balance; + const balance = insertUserAssetBalance + ? getUserAssetsStore(walletAddress as Address) + ?.getState() + .getUserAsset(uniqueId)?.balance || asset.balance + : asset.balance; return { ...asset, diff --git a/src/components/DappBrowser/control-panel/ControlPanel.tsx b/src/components/DappBrowser/control-panel/ControlPanel.tsx index 4f0b56593b2..40bd096d27d 100644 --- a/src/components/DappBrowser/control-panel/ControlPanel.tsx +++ b/src/components/DappBrowser/control-panel/ControlPanel.tsx @@ -40,7 +40,7 @@ import { getHighContrastTextColorWorklet } from '@/worklets/colors'; import { TOP_INSET } from '../Dimensions'; import { formatUrl } from '../utils'; import { RouteProp, useRoute } from '@react-navigation/native'; -import { toHex } from 'viem'; +import { Address, toHex } from 'viem'; import { RainbowNetworks } from '@/networks'; import * as i18n from '@/languages'; import { useDispatch } from 'react-redux'; @@ -61,7 +61,7 @@ import { addressSetSelected, walletsSetSelected } from '@/redux/wallets'; import { getRemoteConfig } from '@/model/remoteConfig'; import { SWAPS_V2, useExperimentalFlag } from '@/config'; import { swapsStore } from '@/state/swaps/swapsStore'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore } from '@/state/assets/userAssets'; import { greaterThan } from '@/helpers/utilities'; const PAGES = { @@ -450,7 +450,9 @@ const HomePanel = ({ if (swaps_v2 || swapsV2Enabled) { swapsStore.setState({ - inputAsset: userAssetsStore.getState().getHighestValueEth(), + inputAsset: getUserAssetsStore(accountAddress as Address) + ?.getState() + .getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); @@ -466,7 +468,7 @@ const HomePanel = ({ }, screen: Routes.MAIN_EXCHANGE_SCREEN, }); - }, [navigate, runWalletChecksBeforeSwapOrBridge, selectedWallet?.uniqueId, swapsV2Enabled]); + }, [accountAddress, navigate, runWalletChecksBeforeSwapOrBridge, selectedWallet?.uniqueId, swapsV2Enabled]); const handleOnPressBridge = useCallback(async () => { const valid = await runWalletChecksBeforeSwapOrBridge(); @@ -478,7 +480,9 @@ const HomePanel = ({ // TODO: We need to set something in swapsStore that deliniates between a swap and bridge // for now let's just treat it like a normal swap swapsStore.setState({ - inputAsset: userAssetsStore.getState().getHighestValueEth(), + inputAsset: getUserAssetsStore(accountAddress as Address) + ?.getState() + .getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); @@ -494,7 +498,7 @@ const HomePanel = ({ }, screen: Routes.MAIN_EXCHANGE_SCREEN, }); - }, [navigate, runWalletChecksBeforeSwapOrBridge, selectedWallet?.uniqueId, swapsV2Enabled]); + }, [accountAddress, navigate, runWalletChecksBeforeSwapOrBridge, selectedWallet?.uniqueId, swapsV2Enabled]); const isOnHomepage = useBrowserStore(state => (state.getActiveTabUrl() || DEFAULT_TAB_URL) === RAINBOW_HOME); diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx index 723fef826aa..d63737214c8 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx @@ -20,7 +20,8 @@ import { useAccountAccentColor } from '@/hooks/useAccountAccentColor'; import { addressCopiedToastAtom } from '@/recoil/addressCopiedToastAtom'; import { Network } from '@/networks/types'; import { swapsStore } from '@/state/swaps/swapsStore'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore } from '@/state/assets/userAssets'; +import { Address } from 'viem'; export const ProfileActionButtonsRowHeight = 80; @@ -181,7 +182,9 @@ function SwapButton() { }); if (swapsV2Enabled) { swapsStore.setState({ - inputAsset: userAssetsStore.getState().getHighestValueEth(), + inputAsset: getUserAssetsStore(accountAddress as Address) + ?.getState() + .getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); diff --git a/src/components/context-menu-buttons/ChainContextMenu.tsx b/src/components/context-menu-buttons/ChainContextMenu.tsx index cce2522b299..b6f267c6296 100644 --- a/src/components/context-menu-buttons/ChainContextMenu.tsx +++ b/src/components/context-menu-buttons/ChainContextMenu.tsx @@ -5,8 +5,10 @@ import { Bleed, Box, Inline, Text, TextProps } from '@/design-system'; import * as i18n from '@/languages'; import { ChainId, ChainNameDisplay } from '@/__swaps__/types/chains'; import { ethereumUtils, showActionSheetWithOptions } from '@/utils'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; import { chainNameForChainIdWithMainnetSubstitution } from '@/__swaps__/utils/chains'; +import { useAccountSettings } from '@/hooks'; +import { Address } from 'viem'; interface DefaultButtonOptions { iconColor?: TextProps['color']; @@ -40,6 +42,7 @@ export const ChainContextMenu = ({ selectedChainId, showAllNetworksOption = true, }: ChainContextMenuProps) => { + const { accountAddress } = useAccountSettings(); const { iconColor = 'labelSecondary', iconSize = 'icon 13px', @@ -49,7 +52,7 @@ export const ChainContextMenu = ({ textWeight = 'heavy', } = defaultButtonOptions; - const balanceSortedChains = userAssetsStore(state => + const balanceSortedChains = useUserAssetsStore(accountAddress as Address)(state => // eslint-disable-next-line no-nested-ternary chainsToDisplay ? chainsToDisplay : excludeChainsWithNoBalance ? state.getChainsWithBalance() : state.getBalanceSortedChainList() ); diff --git a/src/components/expanded-state/AvailableNetworksv2.tsx b/src/components/expanded-state/AvailableNetworksv2.tsx index f4fcfa4952e..f1005576beb 100644 --- a/src/components/expanded-state/AvailableNetworksv2.tsx +++ b/src/components/expanded-state/AvailableNetworksv2.tsx @@ -9,7 +9,7 @@ import Routes from '@/navigation/routesNames'; import { position } from '@/styles'; import { ethereumUtils, watchingAlert } from '@/utils'; import { CurrencySelectionTypes, ExchangeModalTypes, Network } from '@/helpers'; -import { useSwapCurrencyHandlers, useWallets } from '@/hooks'; +import { useAccountSettings, useSwapCurrencyHandlers, useWallets } from '@/hooks'; import { RainbowToken } from '@/entities'; import { useTheme } from '@/theme'; import { ButtonPressAnimation } from '../animations'; @@ -19,12 +19,13 @@ import { RainbowNetworks, getNetworkObj } from '@/networks'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; import { SWAPS_V2, enableActionsOnReadOnlyWallet, useExperimentalFlag } from '@/config'; import { useRemoteConfig } from '@/model/remoteConfig'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore } from '@/state/assets/userAssets'; import { parseSearchAsset } from '@/__swaps__/utils/assets'; import { AddressOrEth, AssetType } from '@/__swaps__/types/assets'; import { chainNameFromChainId } from '@/__swaps__/utils/chains'; import { swapsStore } from '@/state/swaps/swapsStore'; import { InteractionManager } from 'react-native'; +import { Address } from 'viem'; const NOOP = () => null; @@ -41,6 +42,7 @@ const AvailableNetworksv2 = ({ }) => { const { colors } = useTheme(); const { goBack, navigate } = useNavigation(); + const { accountAddress } = useAccountSettings(); const { swaps_v2 } = useRemoteConfig(); const swapsV2Enabled = useExperimentalFlag(SWAPS_V2); const { isReadOnlyWallet } = useWallets(); @@ -85,7 +87,9 @@ const AvailableNetworksv2 = ({ if (swapsV2Enabled || swaps_v2) { const chainId = ethereumUtils.getChainIdFromNetwork(newAsset.network); const uniqueId = `${newAsset.address}_${chainId}`; - const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); + const userAsset = getUserAssetsStore(accountAddress as Address) + ?.getState() + .userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: { @@ -114,8 +118,8 @@ const AvailableNetworksv2 = ({ userAsset, }); - const largestBalanceSameChainUserAsset = userAssetsStore - .getState() + const largestBalanceSameChainUserAsset = getUserAssetsStore(accountAddress as Address) + ?.getState() .getUserAssets() .find(userAsset => userAsset.chainId === chainId && userAsset.address !== newAsset.address); if (largestBalanceSameChainUserAsset) { @@ -150,7 +154,7 @@ const AvailableNetworksv2 = ({ screen: Routes.CURRENCY_SELECT_SCREEN, }); }, - [asset, goBack, navigate, networks, swapsV2Enabled, swaps_v2, updateInputCurrency] + [accountAddress, asset, goBack, navigate, networks, swapsV2Enabled, swaps_v2, updateInputCurrency] ); const handlePressContextMenu = useCallback( diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 3c99b6fd0e6..c9315b9fbc5 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -1,7 +1,7 @@ import lang from 'i18n-js'; import React, { useCallback } from 'react'; import SheetActionButton from './SheetActionButton'; -import { useExpandedStateNavigation, useSwapCurrencyHandlers, useWallets } from '@/hooks'; +import { useAccountSettings, useExpandedStateNavigation, useSwapCurrencyHandlers, useWallets } from '@/hooks'; import Routes from '@/navigation/routesNames'; import { useTheme } from '@/theme'; import { RainbowToken } from '@/entities'; @@ -9,7 +9,7 @@ import { useRemoteConfig } from '@/model/remoteConfig'; import { useNavigation } from '@/navigation'; import { SWAPS_V2, useExperimentalFlag, enableActionsOnReadOnlyWallet } from '@/config'; import { ethereumUtils, watchingAlert } from '@/utils'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore } from '@/state/assets/userAssets'; import { isSameAsset, parseSearchAsset } from '@/__swaps__/utils/assets'; import { chainNameFromChainId } from '@/__swaps__/utils/chains'; import assetInputTypes from '@/helpers/assetInputTypes'; @@ -17,6 +17,7 @@ import { swapsStore } from '@/state/swaps/swapsStore'; import { InteractionManager } from 'react-native'; import { AddressOrEth, AssetType, ParsedSearchAsset } from '@/__swaps__/types/assets'; import exchangeModalTypes from '@/helpers/exchangeModalTypes'; +import { Address } from 'viem'; type SwapActionButtonProps = { asset: RainbowToken; @@ -31,6 +32,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const { colors } = useTheme(); const { swaps_v2 } = useRemoteConfig(); const { navigate } = useNavigation(); + const { accountAddress } = useAccountSettings(); const swapsV2Enabled = useExperimentalFlag(SWAPS_V2); const { isReadOnlyWallet } = useWallets(); @@ -53,7 +55,9 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const chainId = ethereumUtils.getChainIdFromNetwork(asset.network); const uniqueId = `${asset.address}_${chainId}`; - const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); + const userAsset = getUserAssetsStore(accountAddress as Address) + ?.getState() + .userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: { @@ -87,7 +91,9 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const nativeAssetForChain = await ethereumUtils.getNativeAssetForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); if (nativeAssetForChain && !isSameAsset({ address: nativeAssetForChain.address as AddressOrEth, chainId }, parsedAsset)) { - const userOutputAsset = userAssetsStore.getState().getUserAsset(`${nativeAssetForChain.address}_${chainId}`); + const userOutputAsset = getUserAssetsStore(accountAddress as Address) + ?.getState() + .getUserAsset(`${nativeAssetForChain.address}_${chainId}`); if (userOutputAsset) { swapsStore.setState({ outputAsset: userOutputAsset }); @@ -124,8 +130,8 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc } } } else { - const largestBalanceSameChainUserAsset = userAssetsStore - .getState() + const largestBalanceSameChainUserAsset = getUserAssetsStore(accountAddress as Address) + ?.getState() .getUserAssets() .find(userAsset => userAsset.chainId === chainId && userAsset.address !== asset.address); if (largestBalanceSameChainUserAsset) { diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 90834dde2e0..69ed6128bf8 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -24,7 +24,7 @@ const getDefaultCacheKeys = (): Set => { const CACHE_ITEMS_TO_PRESERVE = getDefaultCacheKeys(); export interface UserAssetsState { - associatedWalletAddress: Address | undefined; + associatedWalletAddress: Address; chainBalances: Map; currentAbortController: AbortController; filter: UserAssetFilter; @@ -120,228 +120,243 @@ 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); - }, +export const createUserAssetsStore = (address: Address) => + createRainbowStore( + (set, get) => ({ + associatedWalletAddress: address, + 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(); - getChainsWithBalance: () => { - const chainBalances = [...get().chainBalances.entries()]; - const chainsWithBalances = chainBalances.filter(([, balance]) => !!balance); - return chainsWithBalances.map(([chainId]) => chainId); - }, + const smallBalanceThreshold = supportedNativeCurrencies[store.getState().settings.nativeCurrency].userAssetsSmallThreshold; - 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(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; - } - }, + 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(address)?.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(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; + 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); - - // 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 }; - }); - }, + setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => { + set(state => { + const newCache = new Map(state.searchCache).set(queryKey, filteredIds); - 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)); - }); - - // Ensure all supported chains are in the map with a fallback value of 0 - SUPPORTED_CHAIN_IDS({ testnetMode: false }).forEach(chainId => { - if (!unsortedChainBalances.has(chainId)) { - unsortedChainBalances.set(chainId, 0); - idsByChain.set(chainId, []); + // 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; + } + } } - }); - - // 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) || []); + 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)); + }); + + // Ensure all supported chains are in the map with a fallback value of 0 + SUPPORTED_CHAIN_IDS({ testnetMode: false }).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(); + + 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])); + + idsByChain.set('all', allIdsArray); + + 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 searchCache = new Map(); + + Array.from(chainBalances.keys()).forEach(userAssetFilter => { + const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); + searchCache.set(`${userAssetFilter}`, filteredIds); + }); + + 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, + }), + serializer: serializeUserAssetsState, + storageKey: `userAssets_${address}`, + version: 1, + } + ); - 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); - - 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 userAssetsStoreCache: Record> = {}; - const searchCache = new Map(); +export const useUserAssetsStore = (address: Address): ReturnType => { + if (!userAssetsStoreCache[address]) { + userAssetsStoreCache[address] = createUserAssetsStore(address); + } - Array.from(chainBalances.keys()).forEach(userAssetFilter => { - const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); - searchCache.set(`${userAssetFilter}`, filteredIds); - }); + return userAssetsStoreCache[address]; +}; - 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, - }), - serializer: serializeUserAssetsState, - storageKey: 'userAssets', - version: 3, - } -); +export const getUserAssetsStore = (address: Address): ReturnType | undefined => { + return userAssetsStoreCache[address]; +}; -function getCurrentCache(): Map { - return userAssetsStore.getState().searchCache; +function getCurrentSearchCache(address: Address): Map | undefined { + return userAssetsStoreCache[address]?.getState().searchCache; } diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index f4573bb7834..235fdb258aa 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -1,7 +1,7 @@ import { memo } from 'react'; import { Address } from 'viem'; import { useAccountSettings } from '@/hooks'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { getUserAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import { selectUserAssetsList, selectorFilterByUserChains } from '@/__swaps__/screens/Swap/resources/_selectors/assets'; import { ParsedSearchAsset } from '@/__swaps__/types/assets'; @@ -11,7 +11,7 @@ import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; export const UserAssetsSync = memo(function UserAssetsSync() { const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); - const userAssetsWalletAddress = userAssetsStore(state => state.associatedWalletAddress); + const userAssetsWalletAddress = useUserAssetsStore(currentAddress as Address)(state => state.associatedWalletAddress); const isSwapsOpen = useSwapsStore(state => state.isSwapsOpen); useUserAssets( @@ -28,13 +28,16 @@ export const UserAssetsSync = memo(function UserAssetsSync() { }), onSuccess: data => { if (!isSwapsOpen || userAssetsWalletAddress !== currentAddress) { - userAssetsStore.getState().setUserAssets(currentAddress as Address, data as ParsedSearchAsset[]); + const userAssetsStore = getUserAssetsStore(currentAddress as Address); + if (userAssetsStore) { + userAssetsStore.getState().setUserAssets(currentAddress as Address, data as ParsedSearchAsset[]); - const inputAsset = userAssetsStore.getState().getHighestValueEth(); - useSwapsStore.setState({ - inputAsset, - selectedOutputChainId: inputAsset?.chainId ?? ChainId.mainnet, - }); + const inputAsset = userAssetsStore.getState().getHighestValueEth(); + useSwapsStore.setState({ + inputAsset, + selectedOutputChainId: inputAsset?.chainId ?? ChainId.mainnet, + }); + } } }, } From 311ecac823ec4c37713d5e4609f4daa0bd9ecd1a Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Tue, 20 Aug 2024 10:33:41 -0400 Subject: [PATCH 02/13] change userAssetsStoreCache type --- src/state/assets/userAssets.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 69ed6128bf8..ff8d2f09c17 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -343,20 +343,20 @@ export const createUserAssetsStore = (address: Address) => } ); -const userAssetsStoreCache: Record> = {}; +const userAssetsStoreCache: Map> = new Map(); export const useUserAssetsStore = (address: Address): ReturnType => { - if (!userAssetsStoreCache[address]) { - userAssetsStoreCache[address] = createUserAssetsStore(address); + if (!userAssetsStoreCache.get(address)) { + userAssetsStoreCache.set(address, createUserAssetsStore(address)); } - return userAssetsStoreCache[address]; + return userAssetsStoreCache.get(address) as ReturnType; }; export const getUserAssetsStore = (address: Address): ReturnType | undefined => { - return userAssetsStoreCache[address]; + return userAssetsStoreCache.get(address); }; function getCurrentSearchCache(address: Address): Map | undefined { - return userAssetsStoreCache[address]?.getState().searchCache; + return userAssetsStoreCache.get(address)?.getState().searchCache; } From 84f6f39bd8aa7efcb8278ad7805050b485fcfd73 Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Tue, 20 Aug 2024 13:01:53 -0400 Subject: [PATCH 03/13] mid refactor --- src/state/assets/userAssets.ts | 78 ++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index ff8d2f09c17..c3c4439d1fd 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -6,6 +6,8 @@ import { createRainbowStore } from '@/state/internal/createRainbowStore'; import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; import { ChainId } from '@/__swaps__/types/chains'; import { swapsStore } from '../swaps/swapsStore'; +import { useStore } from 'zustand'; +import { useCallback } from 'react'; const SEARCH_CACHE_MAX_ENTRIES = 50; @@ -343,20 +345,78 @@ export const createUserAssetsStore = (address: Address) => } ); -const userAssetsStoreCache: Map> = new Map(); +type UserAssetsStoreType = ReturnType; -export const useUserAssetsStore = (address: Address): ReturnType => { - if (!userAssetsStoreCache.get(address)) { - userAssetsStoreCache.set(address, createUserAssetsStore(address)); +interface StoreManagerState { + stores: Map; +} + +function serializeStoreManager(state: StoreManagerState, version?: number) { + try { + const x = Array.from(state.stores.entries()).map(([address, store]) => { + const storeState = store.getState(); + const transformedStore = { + ...storeState, + chainBalances: storeState.chainBalances ? Array.from(storeState.chainBalances.entries()) : [], + idsByChain: storeState.idsByChain ? Array.from(storeState.idsByChain.entries()) : [], + userAssets: storeState.userAssets ? Array.from(storeState.userAssets.entries()) : [], + }; + }); + + const transformedStateToPersist: UserAssetsStateWithTransforms = { + ...state, + chainBalances: state.chainBalances ? Array.from(state.chainBalances.entries()) : [], + idsByChain: state.idsByChain ? Array.from(state.idsByChain.entries()) : [], + userAssets: state.userAssets ? Array.from(state.userAssets.entries()) : [], + }; + + return JSON.stringify({ + state: transformedStateToPersist, + version, + }); + } catch (error) { + logger.error(new RainbowError('Failed to serialize state for user assets storage'), { error }); + throw error; } +} - return userAssetsStoreCache.get(address) as ReturnType; -}; +const storeManager = createRainbowStore( + () => ({ + stores: new Map(), + }), + { + storageKey: 'userAssetsStoreManager', + version: 1, + serialize: serializeStoreManager, + deserialize: deserializeStoreManager, + } +); -export const getUserAssetsStore = (address: Address): ReturnType | undefined => { - return userAssetsStoreCache.get(address); +function getOrCreateStore(address: Address): UserAssetsStoreType { + const { stores } = storeManager.getState(); + let store = stores.get(address); + + if (!store) { + store = createUserAssetsStore(address); + storeManager.setState(state => ({ + stores: new Map(state.stores).set(address, store as UserAssetsStoreType), + })); + } + + return store; +} + +export const userAssetsStore = { + getState: (address: Address) => getOrCreateStore(address).getState(), + setState: (address: Address, partial: Partial | ((state: UserAssetsState) => Partial)) => + getOrCreateStore(address).setState(partial), }; +export function useUserAssetsStore(address: Address, selector: (state: UserAssetsState) => T) { + const store = getOrCreateStore(address); + return useStore(store, useCallback(selector, [address])); +} + function getCurrentSearchCache(address: Address): Map | undefined { - return userAssetsStoreCache.get(address)?.getState().searchCache; + return getOrCreateStore(address).getState().searchCache; } From 5eb757b408558d4201796a122f319d6feadc7bbc Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Tue, 20 Aug 2024 16:30:48 -0400 Subject: [PATCH 04/13] refactor --- src/__swaps__/screens/Swap/Swap.tsx | 72 ++- .../screens/Swap/components/CoinRow.tsx | 2 +- .../screens/Swap/components/SearchInput.tsx | 9 +- .../components/TokenList/ChainSelection.tsx | 8 +- .../components/TokenList/TokenToBuyList.tsx | 27 +- .../components/TokenList/TokenToSellList.tsx | 21 +- .../screens/Swap/hooks/useAssetsToSell.ts | 2 +- .../screens/Swap/providers/swap-provider.tsx | 8 +- src/__swaps__/utils/swaps.ts | 6 +- .../control-panel/ControlPanel.tsx | 10 +- .../ProfileActionButtonsRow.tsx | 6 +- .../context-menu-buttons/ChainContextMenu.tsx | 2 +- .../expanded-state/AvailableNetworksv2.tsx | 10 +- .../sheet-action-buttons/SwapActionButton.tsx | 14 +- src/state/assets/userAssets.ts | 528 +++++++++--------- src/state/sync/UserAssetsSync.tsx | 26 +- 16 files changed, 345 insertions(+), 406 deletions(-) diff --git a/src/__swaps__/screens/Swap/Swap.tsx b/src/__swaps__/screens/Swap/Swap.tsx index b9998a8327f..57db947f2b1 100644 --- a/src/__swaps__/screens/Swap/Swap.tsx +++ b/src/__swaps__/screens/Swap/Swap.tsx @@ -23,12 +23,11 @@ import { SwapAssetType } from '@/__swaps__/types/swap'; import { parseSearchAsset } from '@/__swaps__/utils/assets'; import { AbsolutePortalRoot } from '@/components/AbsolutePortal'; import { useDelayedMount } from '@/hooks/useDelayedMount'; -import { getUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import { SwapWarning } from './components/SwapWarning'; import { clearCustomGasSettings } from './hooks/useCustomGas'; import { SwapProvider, useSwapContext } from './providers/swap-provider'; -import store from '@/redux/store'; import { Address } from 'viem'; import { useAccountSettings } from '@/hooks'; @@ -110,29 +109,25 @@ const useCleanupOnUnmount = () => { useEffect(() => { return () => { - const userAssetsStore = getUserAssetsStore(accountAddress as Address); - - if (userAssetsStore) { - const highestValueEth = userAssetsStore.getState().getHighestValueEth(); - const parsedAsset = highestValueEth - ? parseSearchAsset({ - assetWithPrice: undefined, - searchAsset: highestValueEth, - userAsset: highestValueEth, - }) - : null; - - useSwapsStore.setState({ - inputAsset: parsedAsset, - isSwapsOpen: false, - outputAsset: null, - outputSearchQuery: '', - quote: null, - selectedOutputChainId: parsedAsset?.chainId ?? ChainId.mainnet, - }); - - userAssetsStore.setState({ filter: 'all', inputSearchQuery: '' }); - } + const highestValueEth = userAssetsStore.getState(accountAddress as Address).getHighestValueEth(); + const parsedAsset = highestValueEth + ? parseSearchAsset({ + assetWithPrice: undefined, + searchAsset: highestValueEth, + userAsset: highestValueEth, + }) + : null; + + useSwapsStore.setState({ + inputAsset: parsedAsset, + isSwapsOpen: false, + outputAsset: null, + outputSearchQuery: '', + quote: null, + selectedOutputChainId: parsedAsset?.chainId ?? ChainId.mainnet, + }); + + userAssetsStore.setState(accountAddress as Address, { filter: 'all', inputSearchQuery: '' }); clearCustomGasSettings(); }; @@ -144,25 +139,22 @@ const WalletAddressObserver = () => { const { setAsset } = useSwapContext(); const setNewInputAsset = useCallback(() => { - const userAssetsStore = getUserAssetsStore(accountAddress as Address); - if (userAssetsStore) { - const newHighestValueEth = userAssetsStore.getState().getHighestValueEth(); + const newHighestValueEth = userAssetsStore.getState(accountAddress as Address).getHighestValueEth(); - if (userAssetsStore.getState().filter !== 'all') { - userAssetsStore.setState({ filter: 'all' }); - } + if (userAssetsStore.getState(accountAddress as Address).filter !== 'all') { + userAssetsStore.setState(accountAddress as Address, { filter: 'all' }); + } + + setAsset({ + type: SwapAssetType.inputAsset, + asset: newHighestValueEth, + }); + if (userAssetsStore.getState(accountAddress as Address).userAssets.size === 0) { setAsset({ - type: SwapAssetType.inputAsset, - asset: newHighestValueEth, + type: SwapAssetType.outputAsset, + asset: null, }); - - if (userAssetsStore.getState().userAssets.size === 0) { - setAsset({ - type: SwapAssetType.outputAsset, - asset: null, - }); - } } }, [accountAddress, setAsset]); diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index 32a05d568fb..8907efd3b96 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -70,7 +70,7 @@ interface OutputCoinRowProps extends PartialAsset { type CoinRowProps = InputCoinRowProps | OutputCoinRowProps; export function CoinRow({ isFavorite, onPress, output, uniqueId, walletAddress, ...assetProps }: CoinRowProps) { - const inputAsset = useUserAssetsStore(walletAddress as Address)(state => (output ? undefined : state.getUserAsset(uniqueId))); + const inputAsset = useUserAssetsStore(walletAddress as Address, 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 1e37c17888f..0d55b830936 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 { getUserAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import React from 'react'; import Animated, { @@ -42,7 +42,7 @@ export const SearchInput = ({ const label = useForegroundColor('label'); const labelQuaternary = useForegroundColor('labelQuaternary'); - const onInputSearchQueryChange = useUserAssetsStore(accountAddress as Address)(state => state.setSearchQuery); + const onInputSearchQueryChange = useUserAssetsStore(accountAddress as Address, state => state.setSearchQuery); const onOutputSearchQueryChange = useDebouncedCallback((text: string) => useSwapsStore.setState({ outputSearchQuery: text }), 100, { leading: false, @@ -111,9 +111,8 @@ export const SearchInput = ({ useSwapsStore.setState({ outputSearchQuery: '' }); } } else { - const userAssetsStore = getUserAssetsStore(accountAddress as Address); - if (userAssetsStore && userAssetsStore.getState().inputSearchQuery !== '') { - userAssetsStore.getState().setSearchQuery(''); + if (userAssetsStore.getState(accountAddress as Address).inputSearchQuery !== '') { + userAssetsStore.getState(accountAddress as Address).setSearchQuery(''); } } } diff --git a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx index 66efe1c3b58..273a5bd7349 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, useAccountSettings } from '@/hooks'; import { useSharedValueState } from '@/hooks/reanimated/useSharedValueState'; -import { getUserAssetsStore, useUserAssetsStore } 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'; @@ -33,7 +33,7 @@ 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, filter } = useUserAssetsStore(accountAddress as Address)(state => ({ + const { balanceSortedChainList, filter } = useUserAssetsStore(accountAddress as Address, state => ({ balanceSortedChainList: output ? state.getBalanceSortedChainList() : state.getChainsWithBalance(), filter: state.filter, })); @@ -68,7 +68,7 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: setSelectedOutputChainId(Number(actionKey) as ChainId); } else { inputListFilter.value = actionKey === 'all' ? 'all' : (Number(actionKey) as ChainId); - getUserAssetsStore(accountAddress as Address)?.setState({ + userAssetsStore.setState(accountAddress as Address, { filter: actionKey === 'all' ? 'all' : (Number(actionKey) as ChainId), }); } @@ -195,7 +195,7 @@ const ChainButtonIcon = ({ output }: { output: boolean | undefined }) => { const { selectedOutputChainId: animatedSelectedOutputChainId } = useSwapContext(); const { accountAddress } = useAccountSettings(); - const userAssetsFilter = useUserAssetsStore(accountAddress as Address)(state => (output ? undefined : state.filter)); + const userAssetsFilter = useUserAssetsStore(accountAddress as Address, state => (output ? undefined : state.filter)); const selectedOutputChainId = useSharedValueState(animatedSelectedOutputChainId, { pauseSync: !output }); return ( diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx index d6cf27a0a06..cc2db39d961 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx @@ -13,7 +13,7 @@ import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; import { Box, Inline, Stack, Text, TextIcon, useColorMode } from '@/design-system'; import { palettes } from '@/design-system/color/palettes'; import * as i18n from '@/languages'; -import { getUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; import { FlashList } from '@shopify/flash-list'; @@ -102,20 +102,17 @@ export const TokenToBuyList = () => { } })(); - const userAssetsStore = getUserAssetsStore(accountAddress as Address); - if (userAssetsStore) { - const userAsset = userAssetsStore.getState().getUserAsset(token.uniqueId); - const parsedAsset = parseSearchAsset({ - assetWithPrice: undefined, - searchAsset: token, - userAsset: userAsset ?? undefined, - }); - - setAsset({ - type: SwapAssetType.outputAsset, - asset: parsedAsset, - }); - } + const userAsset = userAssetsStore.getState(accountAddress as Address).getUserAsset(token.uniqueId); + const parsedAsset = parseSearchAsset({ + assetWithPrice: undefined, + searchAsset: token, + userAsset: userAsset ?? undefined, + }); + + setAsset({ + type: SwapAssetType.outputAsset, + asset: parsedAsset, + }); const { outputSearchQuery } = swapsStore.getState(); diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx index fdb5c40c7ea..e5fd09a1391 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx @@ -7,7 +7,7 @@ import { getStandardizedUniqueIdWorklet } from '@/__swaps__/utils/swaps'; import { analyticsV2 } from '@/analytics'; import { useDelayedMount } from '@/hooks/useDelayedMount'; import * as i18n from '@/languages'; -import { getUserAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; import { FlashList } from '@shopify/flash-list'; @@ -35,7 +35,7 @@ const TokenToSellListComponent = () => { const { inputProgress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset } = useSwapContext(); const { accountAddress } = useAccountSettings(); - const userAssetIds = useUserAssetsStore(accountAddress as Address)(state => state.getFilteredUserAssetIds()); + const userAssetIds = useUserAssetsStore(accountAddress as Address, state => state.getFilteredUserAssetIds()); const handleSelectToken = useCallback( (token: ParsedSearchAsset | null) => { @@ -56,17 +56,14 @@ const TokenToSellListComponent = () => { asset: token, }); - const userAssetsStore = getUserAssetsStore(accountAddress as Address); - if (userAssetsStore) { - const { inputSearchQuery } = userAssetsStore.getState(); + const { inputSearchQuery } = userAssetsStore.getState(accountAddress as Address); - // track what search query the user had prior to selecting an asset - if (inputSearchQuery.trim().length) { - analyticsV2.track(analyticsV2.event.swapsSearchedForToken, { - query: inputSearchQuery, - type: 'input', - }); - } + // track what search query the user had prior to selecting an asset + if (inputSearchQuery.trim().length) { + analyticsV2.track(analyticsV2.event.swapsSearchedForToken, { + query: inputSearchQuery, + type: 'input', + }); } }, [accountAddress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset] diff --git a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index 2070496e624..6831423dee8 100644 --- a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts +++ b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts @@ -23,7 +23,7 @@ const sortBy = (by: UserAssetFilter) => { export const useAssetsToSell = () => { const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); - const { filter, searchQuery } = useUserAssetsStore(currentAddress as Address)(state => ({ + const { filter, searchQuery } = useUserAssetsStore(currentAddress as Address, state => ({ filter: state.filter, searchQuery: state.inputSearchQuery, })); diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 6752ab6cd1b..24a79a21d79 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -43,7 +43,7 @@ import { walletExecuteRap } from '@/raps/execute'; import { QuoteTypeMap, RapSwapActionParameters } from '@/raps/references'; import { queryClient } from '@/react-query'; import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; -import { getUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { haptics } from '@/utils'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; @@ -599,11 +599,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { ? { ...asset, balance: - (asset && - getUserAssetsStore(accountAddress as Address) - ?.getState() - .getUserAsset(asset.uniqueId)?.balance) || - asset?.balance, + (asset && userAssetsStore.getState(accountAddress as Address).getUserAsset(asset.uniqueId)?.balance) || asset?.balance, } : asset; diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index bc7d0047890..08ab73468e7 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -19,7 +19,7 @@ import * as i18n from '@/languages'; import { RainbowConfig } from '@/model/remoteConfig'; import store from '@/redux/store'; import { ETH_ADDRESS } from '@/references'; -import { getUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore } from '@/state/assets/userAssets'; import { colors } from '@/styles'; import { BigNumberish } from '@ethersproject/bignumber'; import { CrosschainQuote, ETH_ADDRESS as ETH_ADDRESS_AGGREGATOR, Quote, QuoteParams, SwapType, WRAPPED_ASSET } from '@rainbow-me/swaps'; @@ -600,9 +600,7 @@ export const parseAssetAndExtend = ({ const uniqueId = getStandardizedUniqueIdWorklet({ address: asset.address, chainId: asset.chainId }); const balance = insertUserAssetBalance - ? getUserAssetsStore(walletAddress as Address) - ?.getState() - .getUserAsset(uniqueId)?.balance || asset.balance + ? userAssetsStore.getState(walletAddress as Address).getUserAsset(uniqueId)?.balance || asset.balance : asset.balance; return { diff --git a/src/components/DappBrowser/control-panel/ControlPanel.tsx b/src/components/DappBrowser/control-panel/ControlPanel.tsx index 0bad3b5ba3e..43751f077d3 100644 --- a/src/components/DappBrowser/control-panel/ControlPanel.tsx +++ b/src/components/DappBrowser/control-panel/ControlPanel.tsx @@ -61,7 +61,7 @@ import { addressSetSelected, walletsSetSelected } from '@/redux/wallets'; import { getRemoteConfig } from '@/model/remoteConfig'; import { SWAPS_V2, useExperimentalFlag } from '@/config'; import { swapsStore } from '@/state/swaps/swapsStore'; -import { getUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore } from '@/state/assets/userAssets'; import { greaterThan } from '@/helpers/utilities'; import { ChainId } from '@/__swaps__/types/chains'; @@ -452,9 +452,7 @@ const HomePanel = ({ if (swaps_v2 || swapsV2Enabled) { swapsStore.setState({ - inputAsset: getUserAssetsStore(accountAddress as Address) - ?.getState() - .getHighestValueEth(), + inputAsset: userAssetsStore.getState(accountAddress as Address).getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); @@ -482,9 +480,7 @@ const HomePanel = ({ // TODO: We need to set something in swapsStore that deliniates between a swap and bridge // for now let's just treat it like a normal swap swapsStore.setState({ - inputAsset: getUserAssetsStore(accountAddress as Address) - ?.getState() - .getHighestValueEth(), + inputAsset: userAssetsStore.getState(accountAddress as Address).getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx index bd9152ccf77..b0f0abb0ec0 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx @@ -19,7 +19,7 @@ import { useRemoteConfig } from '@/model/remoteConfig'; import { useAccountAccentColor } from '@/hooks/useAccountAccentColor'; import { addressCopiedToastAtom } from '@/recoil/addressCopiedToastAtom'; import { swapsStore } from '@/state/swaps/swapsStore'; -import { getUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore } from '@/state/assets/userAssets'; import { Address } from 'viem'; import { ChainId } from '@/__swaps__/types/chains'; @@ -182,9 +182,7 @@ function SwapButton() { }); if (swapsV2Enabled) { swapsStore.setState({ - inputAsset: getUserAssetsStore(accountAddress as Address) - ?.getState() - .getHighestValueEth(), + inputAsset: userAssetsStore.getState(accountAddress as Address).getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); diff --git a/src/components/context-menu-buttons/ChainContextMenu.tsx b/src/components/context-menu-buttons/ChainContextMenu.tsx index 8dcff803f03..737e86b98a2 100644 --- a/src/components/context-menu-buttons/ChainContextMenu.tsx +++ b/src/components/context-menu-buttons/ChainContextMenu.tsx @@ -52,7 +52,7 @@ export const ChainContextMenu = ({ textWeight = 'heavy', } = defaultButtonOptions; - const balanceSortedChains = useUserAssetsStore(accountAddress as Address)(state => + const balanceSortedChains = useUserAssetsStore(accountAddress as Address, state => // eslint-disable-next-line no-nested-ternary chainsToDisplay ? chainsToDisplay : excludeChainsWithNoBalance ? state.getChainsWithBalance() : state.getBalanceSortedChainList() ); diff --git a/src/components/expanded-state/AvailableNetworksv2.tsx b/src/components/expanded-state/AvailableNetworksv2.tsx index 8b2669beceb..6c74fb18712 100644 --- a/src/components/expanded-state/AvailableNetworksv2.tsx +++ b/src/components/expanded-state/AvailableNetworksv2.tsx @@ -19,7 +19,7 @@ import { RainbowNetworks, getNetworkObj } from '@/networks'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; import { SWAPS_V2, enableActionsOnReadOnlyWallet, useExperimentalFlag } from '@/config'; import { useRemoteConfig } from '@/model/remoteConfig'; -import { getUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore } from '@/state/assets/userAssets'; import { parseSearchAsset } from '@/__swaps__/utils/assets'; import { AddressOrEth, AssetType } from '@/__swaps__/types/assets'; import { chainNameFromChainId } from '@/__swaps__/utils/chains'; @@ -88,9 +88,7 @@ const AvailableNetworksv2 = ({ if (swapsV2Enabled || swaps_v2) { const chainId = ethereumUtils.getChainIdFromNetwork(newAsset.network); const uniqueId = `${newAsset.address}_${chainId}`; - const userAsset = getUserAssetsStore(accountAddress as Address) - ?.getState() - .userAssets.get(uniqueId); + const userAsset = userAssetsStore.getState(accountAddress as Address).userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: { @@ -119,8 +117,8 @@ const AvailableNetworksv2 = ({ userAsset, }); - const largestBalanceSameChainUserAsset = getUserAssetsStore(accountAddress as Address) - ?.getState() + const largestBalanceSameChainUserAsset = userAssetsStore + .getState(accountAddress as Address) .getUserAssets() .find(userAsset => userAsset.chainId === chainId && userAsset.address !== newAsset.address); if (largestBalanceSameChainUserAsset) { diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 131d6e0332f..90b005cca38 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -9,7 +9,7 @@ import { useRemoteConfig } from '@/model/remoteConfig'; import { useNavigation } from '@/navigation'; import { SWAPS_V2, useExperimentalFlag, enableActionsOnReadOnlyWallet } from '@/config'; import { ethereumUtils, watchingAlert } from '@/utils'; -import { getUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore } from '@/state/assets/userAssets'; import { isSameAsset, parseSearchAsset } from '@/__swaps__/utils/assets'; import { chainNameFromChainId } from '@/__swaps__/utils/chains'; import assetInputTypes from '@/helpers/assetInputTypes'; @@ -55,9 +55,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const chainId = ethereumUtils.getChainIdFromNetwork(asset.network); const uniqueId = `${asset.address}_${chainId}`; - const userAsset = getUserAssetsStore(accountAddress as Address) - ?.getState() - .userAssets.get(uniqueId); + const userAsset = userAssetsStore.getState(accountAddress as Address).userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: { @@ -91,8 +89,8 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const nativeAssetForChain = await ethereumUtils.getNativeAssetForNetwork(chainId); if (nativeAssetForChain && !isSameAsset({ address: nativeAssetForChain.address as AddressOrEth, chainId }, parsedAsset)) { - const userOutputAsset = getUserAssetsStore(accountAddress as Address) - ?.getState() + const userOutputAsset = userAssetsStore + .getState(accountAddress as Address) .getUserAsset(`${nativeAssetForChain.address}_${chainId}`); if (userOutputAsset) { @@ -130,8 +128,8 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc } } } else { - const largestBalanceSameChainUserAsset = getUserAssetsStore(accountAddress as Address) - ?.getState() + const largestBalanceSameChainUserAsset = userAssetsStore + .getState(accountAddress as Address) .getUserAssets() .find(userAsset => userAsset.chainId === chainId && userAsset.address !== asset.address); if (largestBalanceSameChainUserAsset) { diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index c3c4439d1fd..2a496910e9c 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -26,7 +26,6 @@ const getDefaultCacheKeys = (): Set => { const CACHE_ITEMS_TO_PRESERVE = getDefaultCacheKeys(); export interface UserAssetsState { - associatedWalletAddress: Address; chainBalances: Map; currentAbortController: AbortController; filter: UserAssetFilter; @@ -44,306 +43,215 @@ 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]> -type UserAssetsStateWithTransforms = Omit, 'chainBalances' | 'idsByChain' | 'userAssets'> & { - chainBalances: Array<[ChainId, number]>; - idsByChain: Array<[UserAssetFilter, UniqueId[]]>; - userAssets: Array<[UniqueId, ParsedSearchAsset]>; -}; +export const createUserAssetsStore = (address: Address) => + 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); + }, -function serializeUserAssetsState(state: Partial, version?: number) { - try { - const transformedStateToPersist: UserAssetsStateWithTransforms = { - ...state, - chainBalances: state.chainBalances ? Array.from(state.chainBalances.entries()) : [], - idsByChain: state.idsByChain ? Array.from(state.idsByChain.entries()) : [], - userAssets: state.userAssets ? Array.from(state.userAssets.entries()) : [], - }; + getChainsWithBalance: () => { + const chainBalances = [...get().chainBalances.entries()]; + const chainsWithBalances = chainBalances.filter(([, balance]) => !!balance); + return chainsWithBalances.map(([chainId]) => chainId); + }, - return JSON.stringify({ - state: transformedStateToPersist, - version, - }); - } catch (error) { - logger.error(new RainbowError('Failed to serialize state for user assets storage'), { error }); - throw error; - } -} + 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 = getCurrentSearchCache(address)?.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(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; + } + }, -function deserializeUserAssetsState(serializedState: string) { - let parsedState: { state: UserAssetsStateWithTransforms; version: number }; - try { - parsedState = JSON.parse(serializedState); - } catch (error) { - logger.error(new RainbowError('Failed to parse serialized state from user assets storage'), { error }); - throw error; - } + getHighestValueEth: () => { + const preferredNetwork = swapsStore.getState().preferredNetwork; + const assets = get().userAssets; - const { state, version } = parsedState; + let highestValueEth = null; - let chainBalances = new Map(); - try { - if (state.chainBalances) { - chainBalances = new Map(state.chainBalances); - } - } catch (error) { - logger.error(new RainbowError('Failed to convert chainBalances from user assets storage'), { error }); - } + for (const [, asset] of assets) { + if (asset.mainnetAddress !== ETH_ADDRESS) continue; - let idsByChain = new Map(); - try { - if (state.idsByChain) { - idsByChain = new Map(state.idsByChain); - } - } catch (error) { - logger.error(new RainbowError('Failed to convert idsByChain from user assets storage'), { error }); - } + if (preferredNetwork && asset.chainId === preferredNetwork) { + return asset; + } - let userAssetsData: Map = new Map(); - try { - if (state.userAssets.length) { - userAssetsData = new Map(state.userAssets); - } - } catch (error) { - logger.error(new RainbowError('Failed to convert userAssets from user assets storage'), { error }); - } + if (!highestValueEth || asset.balance > highestValueEth.balance) { + highestValueEth = asset; + } + } - return { - state: { - ...state, - chainBalances, - idsByChain, - userAssets: userAssetsData, + return highestValueEth; }, - version, - }; -} - -export const createUserAssetsStore = (address: Address) => - createRainbowStore( - (set, get) => ({ - associatedWalletAddress: address, - 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 = getCurrentSearchCache(address)?.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(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; - } - }, + getUserAsset: (uniqueId: UniqueId) => get().userAssets.get(uniqueId) || null, - getHighestValueEth: () => { - const preferredNetwork = swapsStore.getState().preferredNetwork; - const assets = get().userAssets; + getUserAssets: () => Array.from(get().userAssets.values()) || [], - let highestValueEth = null; + selectUserAssetIds: function* (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) { + const { currentAbortController, idsByChain, userAssets } = get(); - for (const [, asset] of assets) { - if (asset.mainnetAddress !== ETH_ADDRESS) continue; + const assetIds = filter ? idsByChain.get(filter) || [] : idsByChain.get('all') || []; - if (preferredNetwork && asset.chainId === preferredNetwork) { - return asset; - } - - if (!highestValueEth || asset.balance > highestValueEth.balance) { - highestValueEth = asset; - } + for (const id of assetIds) { + if (currentAbortController?.signal.aborted) { + return; } + const asset = userAssets.get(id); + if (asset && selector(asset)) { + yield id; + } + } + }, - return highestValueEth; - }, - - getUserAsset: (uniqueId: UniqueId) => get().userAssets.get(uniqueId) || null, + selectUserAssets: function* (selector: (asset: ParsedSearchAsset) => boolean) { + const { currentAbortController, userAssets } = get(); - getUserAssets: () => Array.from(get().userAssets.values()) || [], + for (const [id, asset] of userAssets) { + if (currentAbortController?.signal.aborted) { + return; + } + if (selector(asset)) { + yield [id, asset]; + } + } + }, - selectUserAssetIds: function* (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) { - const { currentAbortController, idsByChain, userAssets } = get(); + setSearchQuery: query => + set(state => { + const { currentAbortController } = state; - const assetIds = filter ? idsByChain.get(filter) || [] : idsByChain.get('all') || []; + // Abort any ongoing search work + currentAbortController.abort(); - for (const id of assetIds) { - if (currentAbortController?.signal.aborted) { - return; - } - const asset = userAssets.get(id); - if (asset && selector(asset)) { - yield id; - } - } - }, + // Create a new AbortController for the new query + const abortController = new AbortController(); - selectUserAssets: function* (selector: (asset: ParsedSearchAsset) => boolean) { - const { currentAbortController, userAssets } = get(); + return { inputSearchQuery: query.trim().toLowerCase(), currentAbortController: abortController }; + }), - for (const [id, asset] of userAssets) { - if (currentAbortController?.signal.aborted) { - return; - } - if (selector(asset)) { - yield [id, asset]; + 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; + } } } - }, - - setSearchQuery: query => - set(state => { - const { currentAbortController } = state; - // Abort any ongoing search work - currentAbortController.abort(); - - // Create a new AbortController for the new query - const abortController = new AbortController(); + return { searchCache: newCache }; + }); + }, - return { inputSearchQuery: query.trim().toLowerCase(), currentAbortController: abortController }; - }), + setUserAssets: (userAssets: Map | ParsedSearchAsset[]) => + set(() => { + const idsByChain = new Map(); + const unsortedChainBalances = new Map(); - setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => { - set(state => { - const newCache = new Map(state.searchCache).set(queryKey, filteredIds); + 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)); + }); - // 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; - } - } + // Ensure all supported chains are in the map with a fallback value of 0 + SUPPORTED_CHAIN_IDS({ testnetMode: false }).forEach(chainId => { + if (!unsortedChainBalances.has(chainId)) { + unsortedChainBalances.set(chainId, 0); + idsByChain.set(chainId, []); } - - 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)); - }); - - // Ensure all supported chains are in the map with a fallback value of 0 - SUPPORTED_CHAIN_IDS({ testnetMode: false }).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) || []); - }); - - 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])); + sortedEntries.forEach(([chainId, balance]) => { + chainBalances.set(chainId, balance); + idsByChain.set(chainId, idsByChain.get(chainId) || []); + }); - idsByChain.set('all', allIdsArray); + 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 smallBalanceThreshold = supportedNativeCurrencies[store.getState().settings.nativeCurrency].userAssetsSmallThreshold; + idsByChain.set('all', allIdsArray); - const filteredAllIdsArray = allIdsArray.filter(id => { - const asset = userAssetsMap.get(id); - return asset && (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold; - }); + const smallBalanceThreshold = supportedNativeCurrencies[store.getState().settings.nativeCurrency].userAssetsSmallThreshold; - const searchCache = new Map(); + const filteredAllIdsArray = allIdsArray.filter(id => { + const asset = userAssetsMap.get(id); + return asset && (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold; + }); - Array.from(chainBalances.keys()).forEach(userAssetFilter => { - const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); - searchCache.set(`${userAssetFilter}`, filteredIds); - }); + const searchCache = new Map(); - searchCache.set('all', filteredAllIdsArray); + Array.from(chainBalances.keys()).forEach(userAssetFilter => { + const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); + searchCache.set(`${userAssetFilter}`, filteredIds); + }); - 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, + searchCache.set('all', filteredAllIdsArray); + + if (isMap) { + return { chainBalances, idsByChain, searchCache, userAssets }; + } else + return { + chainBalances, + idsByChain, + searchCache, + userAssets: userAssetsMap, + }; }), - serializer: serializeUserAssetsState, - storageKey: `userAssets_${address}`, - version: 1, - } - ); + })); type UserAssetsStoreType = ReturnType; @@ -351,23 +259,29 @@ interface StoreManagerState { stores: Map; } -function serializeStoreManager(state: StoreManagerState, version?: number) { - try { - const x = Array.from(state.stores.entries()).map(([address, store]) => { - const storeState = store.getState(); - const transformedStore = { - ...storeState, - chainBalances: storeState.chainBalances ? Array.from(storeState.chainBalances.entries()) : [], - idsByChain: storeState.idsByChain ? Array.from(storeState.idsByChain.entries()) : [], - userAssets: storeState.userAssets ? Array.from(storeState.userAssets.entries()) : [], - }; - }); +// NOTE: We are serializing Map as an Array<[UniqueId, ParsedSearchAsset]> +type UserAssetsStateWithTransforms = Omit, 'chainBalances' | 'idsByChain' | 'userAssets'> & { + chainBalances: Array<[ChainId, number]>; + idsByChain: Array<[UserAssetFilter, UniqueId[]]>; + userAssets: Array<[UniqueId, ParsedSearchAsset]>; +}; + +type StoreManagerStateWithTransforms = { stores: Array<[Address, UserAssetsStateWithTransforms]> }; - const transformedStateToPersist: UserAssetsStateWithTransforms = { - ...state, - chainBalances: state.chainBalances ? Array.from(state.chainBalances.entries()) : [], - idsByChain: state.idsByChain ? Array.from(state.idsByChain.entries()) : [], - userAssets: state.userAssets ? Array.from(state.userAssets.entries()) : [], +function serializeStoreManager(state: Partial, version?: number) { + try { + const transformedStateToPersist: StoreManagerStateWithTransforms = { + stores: state.stores + ? Array.from(state.stores.entries()).map(([address, store]) => { + const storeState = store.getState(); + const transformedStore = { + chainBalances: storeState.chainBalances ? Array.from(storeState.chainBalances.entries()) : [], + idsByChain: storeState.idsByChain ? Array.from(storeState.idsByChain.entries()) : [], + userAssets: storeState.userAssets ? Array.from(storeState.userAssets.entries()) : [], + }; + return [address, transformedStore]; + }) + : [], }; return JSON.stringify({ @@ -380,6 +294,66 @@ function serializeStoreManager(state: StoreManagerState, version?: number) { } } +function deserializeStoreManager(serializedState: string) { + let parsedState: { state: StoreManagerStateWithTransforms; version: number }; + try { + parsedState = JSON.parse(serializedState); + } catch (error) { + logger.error(new RainbowError('Failed to parse serialized state from user assets storage'), { error }); + throw error; + } + + const { state, version } = parsedState; + + const stores = new Map(); + + state.stores.forEach(([address, transformedStore]) => { + let chainBalances = new Map(); + try { + if (transformedStore.chainBalances) { + chainBalances = new Map(transformedStore.chainBalances); + } + } catch (error) { + logger.error(new RainbowError('Failed to convert chainBalances from user assets storage'), { error }); + } + + let idsByChain = new Map(); + try { + if (transformedStore.idsByChain) { + idsByChain = new Map(transformedStore.idsByChain); + } + } catch (error) { + logger.error(new RainbowError('Failed to convert idsByChain from user assets storage'), { error }); + } + + let userAssets: Map = new Map(); + try { + if (transformedStore.userAssets.length) { + userAssets = new Map(transformedStore.userAssets); + } + } catch (error) { + logger.error(new RainbowError('Failed to convert userAssets from user assets storage'), { error }); + } + + const rehydratedStore = createUserAssetsStore(address); + + rehydratedStore.setState({ + chainBalances, + idsByChain, + userAssets, + }); + + stores.set(address, rehydratedStore); + }); + + return { + state: { + stores: stores, + }, + version, + }; +} + const storeManager = createRainbowStore( () => ({ stores: new Map(), @@ -387,8 +361,8 @@ const storeManager = createRainbowStore( { storageKey: 'userAssetsStoreManager', version: 1, - serialize: serializeStoreManager, - deserialize: deserializeStoreManager, + serializer: serializeStoreManager, + deserializer: deserializeStoreManager, } ); diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index 235fdb258aa..0415c8621fe 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -1,7 +1,7 @@ import { memo } from 'react'; import { Address } from 'viem'; import { useAccountSettings } from '@/hooks'; -import { getUserAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import { selectUserAssetsList, selectorFilterByUserChains } from '@/__swaps__/screens/Swap/resources/_selectors/assets'; import { ParsedSearchAsset } from '@/__swaps__/types/assets'; @@ -9,35 +9,31 @@ import { ChainId } from '@/__swaps__/types/chains'; import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; export const UserAssetsSync = memo(function UserAssetsSync() { - const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); + const { accountAddress, nativeCurrency: currentCurrency } = useAccountSettings(); - const userAssetsWalletAddress = useUserAssetsStore(currentAddress as Address)(state => state.associatedWalletAddress); const isSwapsOpen = useSwapsStore(state => state.isSwapsOpen); useUserAssets( { - address: currentAddress as Address, + address: accountAddress as Address, currency: currentCurrency, }, { - enabled: !isSwapsOpen || userAssetsWalletAddress !== currentAddress, + enabled: !isSwapsOpen, select: data => selectorFilterByUserChains({ data, selector: selectUserAssetsList, }), onSuccess: data => { - if (!isSwapsOpen || userAssetsWalletAddress !== currentAddress) { - const userAssetsStore = getUserAssetsStore(currentAddress as Address); - if (userAssetsStore) { - userAssetsStore.getState().setUserAssets(currentAddress as Address, data as ParsedSearchAsset[]); + if (!isSwapsOpen) { + userAssetsStore.getState(accountAddress as Address).setUserAssets(data as ParsedSearchAsset[]); - const inputAsset = userAssetsStore.getState().getHighestValueEth(); - useSwapsStore.setState({ - inputAsset, - selectedOutputChainId: inputAsset?.chainId ?? ChainId.mainnet, - }); - } + const inputAsset = userAssetsStore.getState(accountAddress as Address).getHighestValueEth(); + useSwapsStore.setState({ + inputAsset, + selectedOutputChainId: inputAsset?.chainId ?? ChainId.mainnet, + }); } }, } From ea21a349dbff391e7d8e4823148313f0c4778148 Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Thu, 22 Aug 2024 14:11:17 -0400 Subject: [PATCH 05/13] dont cast to Address --- src/__swaps__/screens/Swap/Swap.tsx | 13 ++++++------- .../screens/Swap/components/SearchInput.tsx | 7 +++---- .../components/TokenList/ChainSelection.tsx | 7 +++---- .../components/TokenList/TokenToBuyList.tsx | 3 +-- .../components/TokenList/TokenToSellList.tsx | 5 ++--- .../screens/Swap/providers/swap-provider.tsx | 3 +-- .../DappBrowser/control-panel/ControlPanel.tsx | 6 +++--- .../profile-header/ProfileActionButtonsRow.tsx | 3 +-- .../expanded-state/AvailableNetworksv2.tsx | 5 ++--- .../sheet-action-buttons/SwapActionButton.tsx | 9 +++------ src/state/assets/userAssets.ts | 18 +++++++++--------- src/state/sync/UserAssetsSync.tsx | 4 ++-- 12 files changed, 36 insertions(+), 47 deletions(-) diff --git a/src/__swaps__/screens/Swap/Swap.tsx b/src/__swaps__/screens/Swap/Swap.tsx index 57db947f2b1..bc76271c8c4 100644 --- a/src/__swaps__/screens/Swap/Swap.tsx +++ b/src/__swaps__/screens/Swap/Swap.tsx @@ -28,7 +28,6 @@ import { useSwapsStore } from '@/state/swaps/swapsStore'; import { SwapWarning } from './components/SwapWarning'; import { clearCustomGasSettings } from './hooks/useCustomGas'; import { SwapProvider, useSwapContext } from './providers/swap-provider'; -import { Address } from 'viem'; import { useAccountSettings } from '@/hooks'; /** README @@ -109,7 +108,7 @@ const useCleanupOnUnmount = () => { useEffect(() => { return () => { - const highestValueEth = userAssetsStore.getState(accountAddress as Address).getHighestValueEth(); + const highestValueEth = userAssetsStore.getState(accountAddress).getHighestValueEth(); const parsedAsset = highestValueEth ? parseSearchAsset({ assetWithPrice: undefined, @@ -127,7 +126,7 @@ const useCleanupOnUnmount = () => { selectedOutputChainId: parsedAsset?.chainId ?? ChainId.mainnet, }); - userAssetsStore.setState(accountAddress as Address, { filter: 'all', inputSearchQuery: '' }); + userAssetsStore.setState(accountAddress, { filter: 'all', inputSearchQuery: '' }); clearCustomGasSettings(); }; @@ -139,10 +138,10 @@ const WalletAddressObserver = () => { const { setAsset } = useSwapContext(); const setNewInputAsset = useCallback(() => { - const newHighestValueEth = userAssetsStore.getState(accountAddress as Address).getHighestValueEth(); + const newHighestValueEth = userAssetsStore.getState(accountAddress).getHighestValueEth(); - if (userAssetsStore.getState(accountAddress as Address).filter !== 'all') { - userAssetsStore.setState(accountAddress as Address, { filter: 'all' }); + if (userAssetsStore.getState(accountAddress).filter !== 'all') { + userAssetsStore.setState(accountAddress, { filter: 'all' }); } setAsset({ @@ -150,7 +149,7 @@ const WalletAddressObserver = () => { asset: newHighestValueEth, }); - if (userAssetsStore.getState(accountAddress as Address).userAssets.size === 0) { + if (userAssetsStore.getState(accountAddress).userAssets.size === 0) { setAsset({ type: SwapAssetType.outputAsset, asset: null, diff --git a/src/__swaps__/screens/Swap/components/SearchInput.tsx b/src/__swaps__/screens/Swap/components/SearchInput.tsx index 0d55b830936..80990cde3f3 100644 --- a/src/__swaps__/screens/Swap/components/SearchInput.tsx +++ b/src/__swaps__/screens/Swap/components/SearchInput.tsx @@ -18,7 +18,6 @@ import Animated, { import { useDebouncedCallback } from 'use-debounce'; import { SearchInputButton } from './SearchInputButton'; import { useAccountSettings } from '@/hooks'; -import { Address } from 'viem'; const AnimatedInput = Animated.createAnimatedComponent(Input); @@ -42,7 +41,7 @@ export const SearchInput = ({ const label = useForegroundColor('label'); const labelQuaternary = useForegroundColor('labelQuaternary'); - const onInputSearchQueryChange = useUserAssetsStore(accountAddress as Address, state => state.setSearchQuery); + const onInputSearchQueryChange = useUserAssetsStore(accountAddress, state => state.setSearchQuery); const onOutputSearchQueryChange = useDebouncedCallback((text: string) => useSwapsStore.setState({ outputSearchQuery: text }), 100, { leading: false, @@ -111,8 +110,8 @@ export const SearchInput = ({ useSwapsStore.setState({ outputSearchQuery: '' }); } } else { - if (userAssetsStore.getState(accountAddress as Address).inputSearchQuery !== '') { - userAssetsStore.getState(accountAddress as Address).setSearchQuery(''); + if (userAssetsStore.getState(accountAddress).inputSearchQuery !== '') { + userAssetsStore.getState(accountAddress).setSearchQuery(''); } } } diff --git a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx index 273a5bd7349..5fa1696b0c8 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx @@ -19,7 +19,6 @@ 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'; -import { Address } from 'viem'; type ChainSelectionProps = { allText?: string; @@ -33,7 +32,7 @@ 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, filter } = useUserAssetsStore(accountAddress as Address, state => ({ + const { balanceSortedChainList, filter } = useUserAssetsStore(accountAddress, state => ({ balanceSortedChainList: output ? state.getBalanceSortedChainList() : state.getChainsWithBalance(), filter: state.filter, })); @@ -68,7 +67,7 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: setSelectedOutputChainId(Number(actionKey) as ChainId); } else { inputListFilter.value = actionKey === 'all' ? 'all' : (Number(actionKey) as ChainId); - userAssetsStore.setState(accountAddress as Address, { + userAssetsStore.setState(accountAddress, { filter: actionKey === 'all' ? 'all' : (Number(actionKey) as ChainId), }); } @@ -195,7 +194,7 @@ const ChainButtonIcon = ({ output }: { output: boolean | undefined }) => { const { selectedOutputChainId: animatedSelectedOutputChainId } = useSwapContext(); const { accountAddress } = useAccountSettings(); - const userAssetsFilter = useUserAssetsStore(accountAddress as Address, state => (output ? undefined : state.filter)); + const userAssetsFilter = useUserAssetsStore(accountAddress, state => (output ? undefined : state.filter)); const selectedOutputChainId = useSharedValueState(animatedSelectedOutputChainId, { pauseSync: !output }); return ( diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx index cc2db39d961..3b488265992 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx @@ -23,7 +23,6 @@ import Animated, { runOnUI, useAnimatedProps, useAnimatedStyle, withTiming } fro import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '../../constants'; import { ChainSelection } from './ChainSelection'; import { useAccountSettings } from '@/hooks'; -import { Address } from 'viem'; export const BUY_LIST_HEADER_HEIGHT = 20 + 10 + 8; // paddingTop + height + paddingBottom @@ -102,7 +101,7 @@ export const TokenToBuyList = () => { } })(); - const userAsset = userAssetsStore.getState(accountAddress as Address).getUserAsset(token.uniqueId); + const userAsset = userAssetsStore.getState(accountAddress).getUserAsset(token.uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: undefined, searchAsset: token, diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx index e5fd09a1391..47d9a3b2713 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx @@ -16,7 +16,6 @@ import Animated, { runOnUI, useAnimatedProps, useAnimatedStyle } from 'react-nat import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '../../constants'; import { ChainSelection } from './ChainSelection'; import { useAccountSettings } from '@/hooks'; -import { Address } from 'viem'; export const SELL_LIST_HEADER_HEIGHT = 20 + 10 + 14; // paddingTop + height + paddingBottom @@ -35,7 +34,7 @@ const TokenToSellListComponent = () => { const { inputProgress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset } = useSwapContext(); const { accountAddress } = useAccountSettings(); - const userAssetIds = useUserAssetsStore(accountAddress as Address, state => state.getFilteredUserAssetIds()); + const userAssetIds = useUserAssetsStore(accountAddress, state => state.getFilteredUserAssetIds()); const handleSelectToken = useCallback( (token: ParsedSearchAsset | null) => { @@ -56,7 +55,7 @@ const TokenToSellListComponent = () => { asset: token, }); - const { inputSearchQuery } = userAssetsStore.getState(accountAddress as Address); + const { inputSearchQuery } = userAssetsStore.getState(accountAddress); // track what search query the user had prior to selecting an asset if (inputSearchQuery.trim().length) { diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 24a79a21d79..c724444fcd2 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -598,8 +598,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const assetToSet = insertUserAssetBalance ? { ...asset, - balance: - (asset && userAssetsStore.getState(accountAddress as Address).getUserAsset(asset.uniqueId)?.balance) || asset?.balance, + balance: (asset && userAssetsStore.getState(accountAddress).getUserAsset(asset.uniqueId)?.balance) || asset?.balance, } : asset; diff --git a/src/components/DappBrowser/control-panel/ControlPanel.tsx b/src/components/DappBrowser/control-panel/ControlPanel.tsx index 43751f077d3..01b5aaffb52 100644 --- a/src/components/DappBrowser/control-panel/ControlPanel.tsx +++ b/src/components/DappBrowser/control-panel/ControlPanel.tsx @@ -40,7 +40,7 @@ import { getHighContrastTextColorWorklet } from '@/worklets/colors'; import { TOP_INSET } from '../Dimensions'; import { formatUrl } from '../utils'; import { RouteProp, useRoute } from '@react-navigation/native'; -import { Address, toHex } from 'viem'; +import { toHex } from 'viem'; import { RainbowNetworks } from '@/networks'; import * as i18n from '@/languages'; import { useDispatch } from 'react-redux'; @@ -452,7 +452,7 @@ const HomePanel = ({ if (swaps_v2 || swapsV2Enabled) { swapsStore.setState({ - inputAsset: userAssetsStore.getState(accountAddress as Address).getHighestValueEth(), + inputAsset: userAssetsStore.getState(accountAddress).getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); @@ -480,7 +480,7 @@ const HomePanel = ({ // TODO: We need to set something in swapsStore that deliniates between a swap and bridge // for now let's just treat it like a normal swap swapsStore.setState({ - inputAsset: userAssetsStore.getState(accountAddress as Address).getHighestValueEth(), + inputAsset: userAssetsStore.getState(accountAddress).getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx index b0f0abb0ec0..bf26f9fb73a 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx @@ -20,7 +20,6 @@ import { useAccountAccentColor } from '@/hooks/useAccountAccentColor'; import { addressCopiedToastAtom } from '@/recoil/addressCopiedToastAtom'; import { swapsStore } from '@/state/swaps/swapsStore'; import { userAssetsStore } from '@/state/assets/userAssets'; -import { Address } from 'viem'; import { ChainId } from '@/__swaps__/types/chains'; export const ProfileActionButtonsRowHeight = 80; @@ -182,7 +181,7 @@ function SwapButton() { }); if (swapsV2Enabled) { swapsStore.setState({ - inputAsset: userAssetsStore.getState(accountAddress as Address).getHighestValueEth(), + inputAsset: userAssetsStore.getState(accountAddress).getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); diff --git a/src/components/expanded-state/AvailableNetworksv2.tsx b/src/components/expanded-state/AvailableNetworksv2.tsx index 6c74fb18712..7f9a54eb465 100644 --- a/src/components/expanded-state/AvailableNetworksv2.tsx +++ b/src/components/expanded-state/AvailableNetworksv2.tsx @@ -25,7 +25,6 @@ import { AddressOrEth, AssetType } from '@/__swaps__/types/assets'; import { chainNameFromChainId } from '@/__swaps__/utils/chains'; import { swapsStore } from '@/state/swaps/swapsStore'; import { InteractionManager } from 'react-native'; -import { Address } from 'viem'; import { ChainId } from '@/__swaps__/types/chains'; const NOOP = () => null; @@ -88,7 +87,7 @@ const AvailableNetworksv2 = ({ if (swapsV2Enabled || swaps_v2) { const chainId = ethereumUtils.getChainIdFromNetwork(newAsset.network); const uniqueId = `${newAsset.address}_${chainId}`; - const userAsset = userAssetsStore.getState(accountAddress as Address).userAssets.get(uniqueId); + const userAsset = userAssetsStore.getState(accountAddress).userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: { @@ -118,7 +117,7 @@ const AvailableNetworksv2 = ({ }); const largestBalanceSameChainUserAsset = userAssetsStore - .getState(accountAddress as Address) + .getState(accountAddress) .getUserAssets() .find(userAsset => userAsset.chainId === chainId && userAsset.address !== newAsset.address); if (largestBalanceSameChainUserAsset) { diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 90b005cca38..44091e1975e 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -17,7 +17,6 @@ import { swapsStore } from '@/state/swaps/swapsStore'; import { InteractionManager } from 'react-native'; import { AddressOrEth, AssetType, ParsedSearchAsset } from '@/__swaps__/types/assets'; import exchangeModalTypes from '@/helpers/exchangeModalTypes'; -import { Address } from 'viem'; type SwapActionButtonProps = { asset: RainbowToken; @@ -55,7 +54,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const chainId = ethereumUtils.getChainIdFromNetwork(asset.network); const uniqueId = `${asset.address}_${chainId}`; - const userAsset = userAssetsStore.getState(accountAddress as Address).userAssets.get(uniqueId); + const userAsset = userAssetsStore.getState(accountAddress).userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: { @@ -89,9 +88,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const nativeAssetForChain = await ethereumUtils.getNativeAssetForNetwork(chainId); if (nativeAssetForChain && !isSameAsset({ address: nativeAssetForChain.address as AddressOrEth, chainId }, parsedAsset)) { - const userOutputAsset = userAssetsStore - .getState(accountAddress as Address) - .getUserAsset(`${nativeAssetForChain.address}_${chainId}`); + const userOutputAsset = userAssetsStore.getState(accountAddress).getUserAsset(`${nativeAssetForChain.address}_${chainId}`); if (userOutputAsset) { swapsStore.setState({ outputAsset: userOutputAsset }); @@ -129,7 +126,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc } } else { const largestBalanceSameChainUserAsset = userAssetsStore - .getState(accountAddress as Address) + .getState(accountAddress) .getUserAssets() .find(userAsset => userAsset.chainId === chainId && userAsset.address !== asset.address); if (largestBalanceSameChainUserAsset) { diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 2a496910e9c..688dc405eca 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -46,7 +46,7 @@ export interface UserAssetsState { setUserAssets: (userAssets: Map | ParsedSearchAsset[]) => void; } -export const createUserAssetsStore = (address: Address) => +export const createUserAssetsStore = (address: Address | string) => createRainbowStore((set, get) => ({ chainBalances: new Map(), currentAbortController: new AbortController(), @@ -256,7 +256,7 @@ export const createUserAssetsStore = (address: Address) => type UserAssetsStoreType = ReturnType; interface StoreManagerState { - stores: Map; + stores: Map
; } // NOTE: We are serializing Map as an Array<[UniqueId, ParsedSearchAsset]> @@ -266,7 +266,7 @@ type UserAssetsStateWithTransforms = Omit, 'chainBalanc userAssets: Array<[UniqueId, ParsedSearchAsset]>; }; -type StoreManagerStateWithTransforms = { stores: Array<[Address, UserAssetsStateWithTransforms]> }; +type StoreManagerStateWithTransforms = { stores: Array<[Address | string, UserAssetsStateWithTransforms]> }; function serializeStoreManager(state: Partial, version?: number) { try { @@ -305,7 +305,7 @@ function deserializeStoreManager(serializedState: string) { const { state, version } = parsedState; - const stores = new Map(); + const stores = new Map
(); state.stores.forEach(([address, transformedStore]) => { let chainBalances = new Map(); @@ -366,7 +366,7 @@ const storeManager = createRainbowStore( } ); -function getOrCreateStore(address: Address): UserAssetsStoreType { +function getOrCreateStore(address: Address | string): UserAssetsStoreType { const { stores } = storeManager.getState(); let store = stores.get(address); @@ -381,16 +381,16 @@ function getOrCreateStore(address: Address): UserAssetsStoreType { } export const userAssetsStore = { - getState: (address: Address) => getOrCreateStore(address).getState(), - setState: (address: Address, partial: Partial | ((state: UserAssetsState) => Partial)) => + getState: (address: Address | string) => getOrCreateStore(address).getState(), + setState: (address: Address | string, partial: Partial | ((state: UserAssetsState) => Partial)) => getOrCreateStore(address).setState(partial), }; -export function useUserAssetsStore(address: Address, selector: (state: UserAssetsState) => T) { +export function useUserAssetsStore(address: Address | string, selector: (state: UserAssetsState) => T) { const store = getOrCreateStore(address); return useStore(store, useCallback(selector, [address])); } -function getCurrentSearchCache(address: Address): Map | undefined { +function getCurrentSearchCache(address: Address | string): Map | undefined { return getOrCreateStore(address).getState().searchCache; } diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index 0415c8621fe..b1fc3ca944a 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -27,9 +27,9 @@ export const UserAssetsSync = memo(function UserAssetsSync() { }), onSuccess: data => { if (!isSwapsOpen) { - userAssetsStore.getState(accountAddress as Address).setUserAssets(data as ParsedSearchAsset[]); + userAssetsStore.getState(accountAddress).setUserAssets(data as ParsedSearchAsset[]); - const inputAsset = userAssetsStore.getState(accountAddress as Address).getHighestValueEth(); + const inputAsset = userAssetsStore.getState(accountAddress).getHighestValueEth(); useSwapsStore.setState({ inputAsset, selectedOutputChainId: inputAsset?.chainId ?? ChainId.mainnet, From 963631d827f294ce603a0bd49022b9d9f4fb5cea Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Thu, 22 Aug 2024 14:13:54 -0400 Subject: [PATCH 06/13] more --- src/__swaps__/screens/Swap/components/CoinRow.tsx | 3 +-- src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts | 2 +- src/components/context-menu-buttons/ChainContextMenu.tsx | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index 8907efd3b96..e0f28847a0c 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -18,7 +18,6 @@ import React, { useCallback, useMemo } from 'react'; import { GestureResponderEvent } from 'react-native'; import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu'; import { SwapCoinIcon } from './SwapCoinIcon'; -import { Address } from 'viem'; export const COIN_ROW_WITH_PADDING_HEIGHT = 56; @@ -70,7 +69,7 @@ interface OutputCoinRowProps extends PartialAsset { type CoinRowProps = InputCoinRowProps | OutputCoinRowProps; export function CoinRow({ isFavorite, onPress, output, uniqueId, walletAddress, ...assetProps }: CoinRowProps) { - const inputAsset = useUserAssetsStore(walletAddress as Address, state => (output ? undefined : state.getUserAsset(uniqueId))); + const inputAsset = useUserAssetsStore(walletAddress, 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/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index 6831423dee8..bc2e8c235fb 100644 --- a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts +++ b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts @@ -23,7 +23,7 @@ const sortBy = (by: UserAssetFilter) => { export const useAssetsToSell = () => { const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); - const { filter, searchQuery } = useUserAssetsStore(currentAddress as Address, state => ({ + const { filter, searchQuery } = useUserAssetsStore(currentAddress, state => ({ filter: state.filter, searchQuery: state.inputSearchQuery, })); diff --git a/src/components/context-menu-buttons/ChainContextMenu.tsx b/src/components/context-menu-buttons/ChainContextMenu.tsx index 737e86b98a2..591ca2d524f 100644 --- a/src/components/context-menu-buttons/ChainContextMenu.tsx +++ b/src/components/context-menu-buttons/ChainContextMenu.tsx @@ -8,7 +8,6 @@ import { useUserAssetsStore } from '@/state/assets/userAssets'; import { showActionSheetWithOptions } from '@/utils'; import { chainNameForChainIdWithMainnetSubstitution } from '@/__swaps__/utils/chains'; import { useAccountSettings } from '@/hooks'; -import { Address } from 'viem'; interface DefaultButtonOptions { iconColor?: TextProps['color']; @@ -52,7 +51,7 @@ export const ChainContextMenu = ({ textWeight = 'heavy', } = defaultButtonOptions; - const balanceSortedChains = useUserAssetsStore(accountAddress as Address, state => + const balanceSortedChains = useUserAssetsStore(accountAddress, state => // eslint-disable-next-line no-nested-ternary chainsToDisplay ? chainsToDisplay : excludeChainsWithNoBalance ? state.getChainsWithBalance() : state.getBalanceSortedChainList() ); From d262ae0fec14ed80f917026ff9ec437181a46d4d Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Thu, 22 Aug 2024 14:18:08 -0400 Subject: [PATCH 07/13] more --- src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts | 3 +-- .../screens/Swap/resources/assets/userAssets.ts | 10 +++++----- .../screens/Swap/resources/assets/userAssetsByChain.ts | 2 +- src/__swaps__/utils/swaps.ts | 3 +-- src/state/sync/UserAssetsSync.tsx | 3 +-- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index bc2e8c235fb..84f02762409 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, @@ -32,7 +31,7 @@ export const useAssetsToSell = () => { const { data: userAssets = [] } = useUserAssets( { - address: currentAddress as Address, + address: currentAddress, currency: currentCurrency, }, { diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index cd0e6abdca8..ad6ee953f8e 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -31,27 +31,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; }; @@ -138,7 +138,7 @@ async function userAssetsQueryFunctionRetryByChain({ currency, testnetMode, }: { - address: Address; + address: Address | string; chainIds: ChainId[]; currency: SupportedCurrencyKey; testnetMode?: boolean; diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts index 9652ecf2ae4..2859d301e47 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/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index 08ab73468e7..55e2001df54 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -40,7 +40,6 @@ import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from import { inputKeys } from '../types/swap'; import { valueBasedDecimalFormatter } from './decimalFormatter'; import { convertAmountToRawAmount } from './numbers'; -import { Address } from 'viem'; // /---- 🎨 Color functions 🎨 ----/ // // @@ -600,7 +599,7 @@ export const parseAssetAndExtend = ({ const uniqueId = getStandardizedUniqueIdWorklet({ address: asset.address, chainId: asset.chainId }); const balance = insertUserAssetBalance - ? userAssetsStore.getState(walletAddress as Address).getUserAsset(uniqueId)?.balance || asset.balance + ? userAssetsStore.getState(walletAddress).getUserAsset(uniqueId)?.balance || asset.balance : asset.balance; return { diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index b1fc3ca944a..a9cc65121c9 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -1,5 +1,4 @@ import { memo } from 'react'; -import { Address } from 'viem'; import { useAccountSettings } from '@/hooks'; import { userAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; @@ -15,7 +14,7 @@ export const UserAssetsSync = memo(function UserAssetsSync() { useUserAssets( { - address: accountAddress as Address, + address: accountAddress, currency: currentCurrency, }, { From 04e70556002694ba87c94d3ce5b2f938b87a0dc3 Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Mon, 26 Aug 2024 12:47:00 -0400 Subject: [PATCH 08/13] rm address param --- src/__swaps__/screens/Swap/Swap.tsx | 18 ++++++------- .../screens/Swap/components/CoinRow.tsx | 6 ++--- .../screens/Swap/components/SearchInput.tsx | 8 +++--- .../components/TokenList/ChainSelection.tsx | 12 ++++----- .../components/TokenList/TokenToBuyList.tsx | 7 ++--- .../components/TokenList/TokenToSellList.tsx | 17 +++--------- .../screens/Swap/hooks/useAssetsToSell.ts | 2 +- .../screens/Swap/providers/swap-provider.tsx | 11 ++++---- src/__swaps__/utils/swaps.ts | 6 +---- .../control-panel/ControlPanel.tsx | 8 +++--- .../ProfileActionButtonsRow.tsx | 2 +- .../context-menu-buttons/ChainContextMenu.tsx | 4 +-- .../expanded-state/AvailableNetworksv2.tsx | 9 +++---- .../sheet-action-buttons/SwapActionButton.tsx | 9 +++---- src/state/assets/userAssets.ts | 27 ++++++++++--------- src/state/sync/UserAssetsSync.tsx | 4 +-- 16 files changed, 61 insertions(+), 89 deletions(-) diff --git a/src/__swaps__/screens/Swap/Swap.tsx b/src/__swaps__/screens/Swap/Swap.tsx index 7a16707cab2..0ac23040971 100644 --- a/src/__swaps__/screens/Swap/Swap.tsx +++ b/src/__swaps__/screens/Swap/Swap.tsx @@ -106,11 +106,9 @@ const useMountSignal = () => { }; const useCleanupOnUnmount = () => { - const { accountAddress } = useAccountSettings(); - useEffect(() => { return () => { - const highestValueEth = userAssetsStore.getState(accountAddress).getHighestValueEth(); + const highestValueEth = userAssetsStore.getState().getHighestValueEth(); const parsedAsset = highestValueEth ? parseSearchAsset({ assetWithPrice: undefined, @@ -128,11 +126,11 @@ const useCleanupOnUnmount = () => { selectedOutputChainId: parsedAsset?.chainId ?? ChainId.mainnet, }); - userAssetsStore.setState(accountAddress, { filter: 'all', inputSearchQuery: '' }); + userAssetsStore.setState({ filter: 'all', inputSearchQuery: '' }); clearCustomGasSettings(); }; - }, [accountAddress]); + }, []); }; const WalletAddressObserver = () => { @@ -140,10 +138,10 @@ const WalletAddressObserver = () => { const { setAsset } = useSwapContext(); const setNewInputAsset = useCallback(() => { - const newHighestValueEth = userAssetsStore.getState(accountAddress).getHighestValueEth(); + const newHighestValueEth = userAssetsStore.getState().getHighestValueEth(); - if (userAssetsStore.getState(accountAddress).filter !== 'all') { - userAssetsStore.setState(accountAddress, { filter: 'all' }); + if (userAssetsStore.getState().filter !== 'all') { + userAssetsStore.setState({ filter: 'all' }); } setAsset({ @@ -151,13 +149,13 @@ const WalletAddressObserver = () => { asset: newHighestValueEth, }); - if (userAssetsStore.getState(accountAddress).userAssets.size === 0) { + if (userAssetsStore.getState().userAssets.size === 0) { setAsset({ type: SwapAssetType.outputAsset, asset: null, }); } - }, [accountAddress, setAsset]); + }, [setAsset]); useAnimatedReaction( () => accountAddress, diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index e0f28847a0c..1598b48ed61 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -52,7 +52,6 @@ interface InputCoinRowProps { onPress: (asset: ParsedSearchAsset | null) => void; output?: false | undefined; uniqueId: string; - walletAddress: string; } type PartialAsset = Pick; @@ -63,13 +62,12 @@ interface OutputCoinRowProps extends PartialAsset { output: true; nativePriceChange?: string; isTrending?: boolean; - walletAddress: string; } type CoinRowProps = InputCoinRowProps | OutputCoinRowProps; -export function CoinRow({ isFavorite, onPress, output, uniqueId, walletAddress, ...assetProps }: CoinRowProps) { - const inputAsset = useUserAssetsStore(walletAddress, state => (output ? undefined : state.getUserAsset(uniqueId))); +export function CoinRow({ isFavorite, onPress, output, uniqueId, ...assetProps }: CoinRowProps) { + 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 80990cde3f3..007a325b090 100644 --- a/src/__swaps__/screens/Swap/components/SearchInput.tsx +++ b/src/__swaps__/screens/Swap/components/SearchInput.tsx @@ -17,7 +17,6 @@ import Animated, { } from 'react-native-reanimated'; import { useDebouncedCallback } from 'use-debounce'; import { SearchInputButton } from './SearchInputButton'; -import { useAccountSettings } from '@/hooks'; const AnimatedInput = Animated.createAnimatedComponent(Input); @@ -35,13 +34,12 @@ export const SearchInput = ({ }) => { const { isDarkMode } = useColorMode(); const { inputProgress, inputSearchRef, outputProgress, outputSearchRef } = useSwapContext(); - const { accountAddress } = useAccountSettings(); const fillTertiary = useForegroundColor('fillTertiary'); const label = useForegroundColor('label'); const labelQuaternary = useForegroundColor('labelQuaternary'); - const onInputSearchQueryChange = useUserAssetsStore(accountAddress, state => state.setSearchQuery); + const onInputSearchQueryChange = useUserAssetsStore(state => state.setSearchQuery); const onOutputSearchQueryChange = useDebouncedCallback((text: string) => useSwapsStore.setState({ outputSearchQuery: text }), 100, { leading: false, @@ -110,8 +108,8 @@ export const SearchInput = ({ useSwapsStore.setState({ outputSearchQuery: '' }); } } else { - if (userAssetsStore.getState(accountAddress).inputSearchQuery !== '') { - userAssetsStore.getState(accountAddress).setSearchQuery(''); + if (userAssetsStore.getState().inputSearchQuery !== '') { + userAssetsStore.getState().setSearchQuery(''); } } } diff --git a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx index 5fa1696b0c8..a84dce7919e 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx @@ -13,7 +13,7 @@ import { analyticsV2 } from '@/analytics'; import { ChainImage } from '@/components/coin-icon/ChainImage'; import { ContextMenuButton } from '@/components/context-menu'; import { AnimatedText, Bleed, Box, Inline, Text, TextIcon, globalColors, useColorMode } from '@/design-system'; -import { useAccountAccentColor, useAccountSettings } from '@/hooks'; +import { useAccountAccentColor } from '@/hooks'; import { useSharedValueState } from '@/hooks/reanimated/useSharedValueState'; import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; @@ -28,11 +28,10 @@ type ChainSelectionProps = { export const ChainSelection = memo(function ChainSelection({ allText, output }: ChainSelectionProps) { const { isDarkMode } = useColorMode(); const { accentColor: accountColor } = useAccountAccentColor(); - const { accountAddress } = useAccountSettings(); const { selectedOutputChainId, setSelectedOutputChainId } = useSwapContext(); // chains sorted by balance on output, chains without balance hidden on input - const { balanceSortedChainList, filter } = useUserAssetsStore(accountAddress, state => ({ + const { balanceSortedChainList, filter } = useUserAssetsStore(state => ({ balanceSortedChainList: output ? state.getBalanceSortedChainList() : state.getChainsWithBalance(), filter: state.filter, })); @@ -67,12 +66,12 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: setSelectedOutputChainId(Number(actionKey) as ChainId); } else { inputListFilter.value = actionKey === 'all' ? 'all' : (Number(actionKey) as ChainId); - userAssetsStore.setState(accountAddress, { + userAssetsStore.setState({ filter: actionKey === 'all' ? 'all' : (Number(actionKey) as ChainId), }); } }, - [accountAddress, inputListFilter, output, setSelectedOutputChainId] + [inputListFilter, output, setSelectedOutputChainId] ); const menuConfig = useMemo(() => { @@ -192,9 +191,8 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: const ChainButtonIcon = ({ output }: { output: boolean | undefined }) => { const { selectedOutputChainId: animatedSelectedOutputChainId } = useSwapContext(); - const { accountAddress } = useAccountSettings(); - const userAssetsFilter = useUserAssetsStore(accountAddress, 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/TokenToBuyList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx index 3b488265992..ee486ec0fe9 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx @@ -22,7 +22,6 @@ import { ScrollViewProps } from 'react-native'; import Animated, { runOnUI, useAnimatedProps, useAnimatedStyle, withTiming } from 'react-native-reanimated'; import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '../../constants'; import { ChainSelection } from './ChainSelection'; -import { useAccountSettings } from '@/hooks'; export const BUY_LIST_HEADER_HEIGHT = 20 + 10 + 8; // paddingTop + height + paddingBottom @@ -87,7 +86,6 @@ const ScrollViewWithRef = forwardRef(function ScrollViewWit export const TokenToBuyList = () => { const { internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, outputProgress, setAsset } = useSwapContext(); const { results: sections, isLoading } = useSearchCurrencyLists(); - const { accountAddress } = useAccountSettings(); const handleSelectToken = useCallback( (token: SearchAsset) => { @@ -101,7 +99,7 @@ export const TokenToBuyList = () => { } })(); - const userAsset = userAssetsStore.getState(accountAddress).getUserAsset(token.uniqueId); + const userAsset = userAssetsStore.getState().getUserAsset(token.uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: undefined, searchAsset: token, @@ -123,7 +121,7 @@ export const TokenToBuyList = () => { }); } }, - [accountAddress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset] + [internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset] ); const animatedListPadding = useAnimatedStyle(() => { @@ -174,7 +172,6 @@ export const TokenToBuyList = () => { output symbol={item.symbol} uniqueId={item.uniqueId} - walletAddress={accountAddress} /> ); }} diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx index 47d9a3b2713..f18b2ac99c9 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx @@ -15,7 +15,6 @@ import React, { useCallback, useMemo } from 'react'; import Animated, { runOnUI, useAnimatedProps, useAnimatedStyle } from 'react-native-reanimated'; import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '../../constants'; import { ChainSelection } from './ChainSelection'; -import { useAccountSettings } from '@/hooks'; export const SELL_LIST_HEADER_HEIGHT = 20 + 10 + 14; // paddingTop + height + paddingBottom @@ -32,9 +31,8 @@ export const TokenToSellList = () => { const TokenToSellListComponent = () => { const { inputProgress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset } = useSwapContext(); - const { accountAddress } = useAccountSettings(); - const userAssetIds = useUserAssetsStore(accountAddress, state => state.getFilteredUserAssetIds()); + const userAssetIds = useUserAssetsStore(state => state.getFilteredUserAssetIds()); const handleSelectToken = useCallback( (token: ParsedSearchAsset | null) => { @@ -55,7 +53,7 @@ const TokenToSellListComponent = () => { asset: token, }); - const { inputSearchQuery } = userAssetsStore.getState(accountAddress); + const { inputSearchQuery } = userAssetsStore.getState(); // track what search query the user had prior to selecting an asset if (inputSearchQuery.trim().length) { @@ -65,7 +63,7 @@ const TokenToSellListComponent = () => { }); } }, - [accountAddress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset] + [internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset] ); const animatedListPadding = useAnimatedStyle(() => { @@ -94,14 +92,7 @@ const TokenToSellListComponent = () => { estimatedListSize={{ height: EXPANDED_INPUT_HEIGHT - 77, width: DEVICE_WIDTH - 24 }} keyExtractor={uniqueId => uniqueId} renderItem={({ item: uniqueId }) => { - return ( - handleSelectToken(asset)} - output={false} - uniqueId={uniqueId} - walletAddress={accountAddress} - /> - ); + return handleSelectToken(asset)} output={false} uniqueId={uniqueId} />; }} renderScrollComponent={props => { return ( diff --git a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index 84f02762409..e78faa661c4 100644 --- a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts +++ b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts @@ -22,7 +22,7 @@ const sortBy = (by: UserAssetFilter) => { export const useAssetsToSell = () => { const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); - const { filter, searchQuery } = useUserAssetsStore(currentAddress, state => ({ + const { filter, searchQuery } = useUserAssetsStore(state => ({ filter: state.filter, searchQuery: state.inputSearchQuery, })); diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index c724444fcd2..b852eb423e3 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -131,7 +131,7 @@ interface SwapProviderProps { } export const SwapProvider = ({ children }: SwapProviderProps) => { - const { accountAddress, nativeCurrency } = useAccountSettings(); + const { nativeCurrency } = useAccountSettings(); const isFetching = useSharedValue(false); const isQuoteStale = useSharedValue(0); // TODO: Convert this to a boolean @@ -146,8 +146,8 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const lastTypedInput = useSharedValue('inputAmount'); const focusedInput = useSharedValue('inputAmount'); - const initialSelectedInputAsset = parseAssetAndExtend({ asset: swapsStore.getState().inputAsset, walletAddress: accountAddress }); - const initialSelectedOutputAsset = parseAssetAndExtend({ asset: swapsStore.getState().outputAsset, walletAddress: accountAddress }); + const initialSelectedInputAsset = parseAssetAndExtend({ asset: swapsStore.getState().inputAsset }); + const initialSelectedOutputAsset = parseAssetAndExtend({ asset: swapsStore.getState().outputAsset }); const internalSelectedInputAsset = useSharedValue(initialSelectedInputAsset); const internalSelectedOutputAsset = useSharedValue(initialSelectedOutputAsset); @@ -552,7 +552,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const setAsset = useCallback( ({ type, asset }: { type: SwapAssetType; asset: ParsedSearchAsset | null }) => { const insertUserAssetBalance = type !== SwapAssetType.inputAsset; - const extendedAsset = parseAssetAndExtend({ asset, insertUserAssetBalance, walletAddress: accountAddress }); + const extendedAsset = parseAssetAndExtend({ asset, insertUserAssetBalance }); const otherSelectedAsset = type === SwapAssetType.inputAsset ? internalSelectedOutputAsset.value : internalSelectedInputAsset.value; const isSameAsOtherAsset = !!(otherSelectedAsset && otherSelectedAsset.uniqueId === extendedAsset?.uniqueId); @@ -598,7 +598,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const assetToSet = insertUserAssetBalance ? { ...asset, - balance: (asset && userAssetsStore.getState(accountAddress).getUserAsset(asset.uniqueId)?.balance) || asset?.balance, + balance: (asset && userAssetsStore.getState().getUserAsset(asset.uniqueId)?.balance) || asset?.balance, } : asset; @@ -650,7 +650,6 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }, [ SwapInputController.quoteFetchingInterval, - accountAddress, handleProgressNavigation, internalSelectedInputAsset.value, internalSelectedOutputAsset.value, diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index 55e2001df54..33071792138 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -569,7 +569,6 @@ export const priceForAsset = ({ type ParseAssetAndExtendProps = { asset: ParsedSearchAsset | null; insertUserAssetBalance?: boolean; - walletAddress: string; }; const ETH_COLORS: Colors = { @@ -586,7 +585,6 @@ export const getStandardizedUniqueIdWorklet = ({ address, chainId }: { address: export const parseAssetAndExtend = ({ asset, insertUserAssetBalance, - walletAddress, }: ParseAssetAndExtendProps): ExtendedAnimatedAssetWithColors | null => { if (!asset) { return null; @@ -598,9 +596,7 @@ export const parseAssetAndExtend = ({ }); const uniqueId = getStandardizedUniqueIdWorklet({ address: asset.address, chainId: asset.chainId }); - const balance = insertUserAssetBalance - ? userAssetsStore.getState(walletAddress).getUserAsset(uniqueId)?.balance || asset.balance - : asset.balance; + const balance = insertUserAssetBalance ? userAssetsStore.getState().getUserAsset(uniqueId)?.balance || asset.balance : asset.balance; return { ...asset, diff --git a/src/components/DappBrowser/control-panel/ControlPanel.tsx b/src/components/DappBrowser/control-panel/ControlPanel.tsx index 01b5aaffb52..c02f018636d 100644 --- a/src/components/DappBrowser/control-panel/ControlPanel.tsx +++ b/src/components/DappBrowser/control-panel/ControlPanel.tsx @@ -452,7 +452,7 @@ const HomePanel = ({ if (swaps_v2 || swapsV2Enabled) { swapsStore.setState({ - inputAsset: userAssetsStore.getState(accountAddress).getHighestValueEth(), + inputAsset: userAssetsStore.getState().getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); @@ -468,7 +468,7 @@ const HomePanel = ({ }, screen: Routes.MAIN_EXCHANGE_SCREEN, }); - }, [accountAddress, navigate, runWalletChecksBeforeSwapOrBridge, selectedWallet?.uniqueId, swapsV2Enabled]); + }, [navigate, runWalletChecksBeforeSwapOrBridge, selectedWallet?.uniqueId, swapsV2Enabled]); const handleOnPressBridge = useCallback(async () => { const valid = await runWalletChecksBeforeSwapOrBridge(); @@ -480,7 +480,7 @@ const HomePanel = ({ // TODO: We need to set something in swapsStore that deliniates between a swap and bridge // for now let's just treat it like a normal swap swapsStore.setState({ - inputAsset: userAssetsStore.getState(accountAddress).getHighestValueEth(), + inputAsset: userAssetsStore.getState().getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); @@ -496,7 +496,7 @@ const HomePanel = ({ }, screen: Routes.MAIN_EXCHANGE_SCREEN, }); - }, [accountAddress, navigate, runWalletChecksBeforeSwapOrBridge, selectedWallet?.uniqueId, swapsV2Enabled]); + }, [navigate, runWalletChecksBeforeSwapOrBridge, selectedWallet?.uniqueId, swapsV2Enabled]); const isOnHomepage = useBrowserStore(state => (state.getActiveTabUrl() || DEFAULT_TAB_URL) === RAINBOW_HOME); diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx index bf26f9fb73a..2378b7c6f5d 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx @@ -181,7 +181,7 @@ function SwapButton() { }); if (swapsV2Enabled) { swapsStore.setState({ - inputAsset: userAssetsStore.getState(accountAddress).getHighestValueEth(), + inputAsset: userAssetsStore.getState().getHighestValueEth(), }); InteractionManager.runAfterInteractions(() => { navigate(Routes.SWAP); diff --git a/src/components/context-menu-buttons/ChainContextMenu.tsx b/src/components/context-menu-buttons/ChainContextMenu.tsx index 591ca2d524f..17a50708cb8 100644 --- a/src/components/context-menu-buttons/ChainContextMenu.tsx +++ b/src/components/context-menu-buttons/ChainContextMenu.tsx @@ -7,7 +7,6 @@ import { ChainId, ChainNameDisplay } from '@/__swaps__/types/chains'; import { useUserAssetsStore } from '@/state/assets/userAssets'; import { showActionSheetWithOptions } from '@/utils'; import { chainNameForChainIdWithMainnetSubstitution } from '@/__swaps__/utils/chains'; -import { useAccountSettings } from '@/hooks'; interface DefaultButtonOptions { iconColor?: TextProps['color']; @@ -41,7 +40,6 @@ export const ChainContextMenu = ({ selectedChainId, showAllNetworksOption = true, }: ChainContextMenuProps) => { - const { accountAddress } = useAccountSettings(); const { iconColor = 'labelSecondary', iconSize = 'icon 13px', @@ -51,7 +49,7 @@ export const ChainContextMenu = ({ textWeight = 'heavy', } = defaultButtonOptions; - const balanceSortedChains = useUserAssetsStore(accountAddress, state => + const balanceSortedChains = useUserAssetsStore(state => // eslint-disable-next-line no-nested-ternary chainsToDisplay ? chainsToDisplay : excludeChainsWithNoBalance ? state.getChainsWithBalance() : state.getBalanceSortedChainList() ); diff --git a/src/components/expanded-state/AvailableNetworksv2.tsx b/src/components/expanded-state/AvailableNetworksv2.tsx index 7f9a54eb465..7e211576537 100644 --- a/src/components/expanded-state/AvailableNetworksv2.tsx +++ b/src/components/expanded-state/AvailableNetworksv2.tsx @@ -9,7 +9,7 @@ import Routes from '@/navigation/routesNames'; import { position } from '@/styles'; import { ethereumUtils, watchingAlert } from '@/utils'; import { CurrencySelectionTypes, ExchangeModalTypes, Network } from '@/helpers'; -import { useAccountSettings, useSwapCurrencyHandlers, useWallets } from '@/hooks'; +import { useSwapCurrencyHandlers, useWallets } from '@/hooks'; import { RainbowToken } from '@/entities'; import { useTheme } from '@/theme'; import { ButtonPressAnimation } from '../animations'; @@ -42,7 +42,6 @@ const AvailableNetworksv2 = ({ }) => { const { colors } = useTheme(); const { goBack, navigate } = useNavigation(); - const { accountAddress } = useAccountSettings(); const { swaps_v2 } = useRemoteConfig(); const swapsV2Enabled = useExperimentalFlag(SWAPS_V2); const { isReadOnlyWallet } = useWallets(); @@ -87,7 +86,7 @@ const AvailableNetworksv2 = ({ if (swapsV2Enabled || swaps_v2) { const chainId = ethereumUtils.getChainIdFromNetwork(newAsset.network); const uniqueId = `${newAsset.address}_${chainId}`; - const userAsset = userAssetsStore.getState(accountAddress).userAssets.get(uniqueId); + const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: { @@ -117,7 +116,7 @@ const AvailableNetworksv2 = ({ }); const largestBalanceSameChainUserAsset = userAssetsStore - .getState(accountAddress) + .getState() .getUserAssets() .find(userAsset => userAsset.chainId === chainId && userAsset.address !== newAsset.address); if (largestBalanceSameChainUserAsset) { @@ -152,7 +151,7 @@ const AvailableNetworksv2 = ({ screen: Routes.CURRENCY_SELECT_SCREEN, }); }, - [accountAddress, asset, goBack, navigate, networks, swapsV2Enabled, swaps_v2, updateInputCurrency] + [asset, goBack, isReadOnlyWallet, navigate, networks, swapsV2Enabled, swaps_v2, updateInputCurrency] ); const handlePressContextMenu = useCallback( diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 44091e1975e..810bb9e7890 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -1,7 +1,7 @@ import lang from 'i18n-js'; import React, { useCallback } from 'react'; import SheetActionButton from './SheetActionButton'; -import { useAccountSettings, useExpandedStateNavigation, useSwapCurrencyHandlers, useWallets } from '@/hooks'; +import { useExpandedStateNavigation, useSwapCurrencyHandlers, useWallets } from '@/hooks'; import Routes from '@/navigation/routesNames'; import { useTheme } from '@/theme'; import { RainbowToken } from '@/entities'; @@ -31,7 +31,6 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const { colors } = useTheme(); const { swaps_v2 } = useRemoteConfig(); const { navigate } = useNavigation(); - const { accountAddress } = useAccountSettings(); const swapsV2Enabled = useExperimentalFlag(SWAPS_V2); const { isReadOnlyWallet } = useWallets(); @@ -54,7 +53,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const chainId = ethereumUtils.getChainIdFromNetwork(asset.network); const uniqueId = `${asset.address}_${chainId}`; - const userAsset = userAssetsStore.getState(accountAddress).userAssets.get(uniqueId); + const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ assetWithPrice: { @@ -88,7 +87,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc const nativeAssetForChain = await ethereumUtils.getNativeAssetForNetwork(chainId); if (nativeAssetForChain && !isSameAsset({ address: nativeAssetForChain.address as AddressOrEth, chainId }, parsedAsset)) { - const userOutputAsset = userAssetsStore.getState(accountAddress).getUserAsset(`${nativeAssetForChain.address}_${chainId}`); + const userOutputAsset = userAssetsStore.getState().getUserAsset(`${nativeAssetForChain.address}_${chainId}`); if (userOutputAsset) { swapsStore.setState({ outputAsset: userOutputAsset }); @@ -126,7 +125,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc } } else { const largestBalanceSameChainUserAsset = userAssetsStore - .getState(accountAddress) + .getState() .getUserAssets() .find(userAsset => userAsset.chainId === chainId && userAsset.address !== asset.address); if (largestBalanceSameChainUserAsset) { diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index f60f2df9eb5..353b4e8525d 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -1,6 +1,6 @@ import { Address } from 'viem'; import { RainbowError, logger } from '@/logger'; -import store from '@/redux/store'; +import reduxStore from '@/redux/store'; import { ETH_ADDRESS, SUPPORTED_CHAIN_IDS, supportedNativeCurrencies } from '@/references'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; @@ -71,13 +71,13 @@ export const createUserAssetsStore = (address: Address | string) => getFilteredUserAssetIds: () => { const { filter, inputSearchQuery: rawSearchQuery, selectUserAssetIds, setSearchCache } = get(); - const smallBalanceThreshold = supportedNativeCurrencies[store.getState().settings.nativeCurrency].userAssetsSmallThreshold; + 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(address)?.get(queryKey); + const cachedData = getCurrentSearchCache()?.get(queryKey); // Check if the search results are already cached if (cachedData) { @@ -225,7 +225,7 @@ export const createUserAssetsStore = (address: Address | string) => idsByChain.set('all', allIdsArray); - const smallBalanceThreshold = supportedNativeCurrencies[store.getState().settings.nativeCurrency].userAssetsSmallThreshold; + const smallBalanceThreshold = supportedNativeCurrencies[reduxStore.getState().settings.nativeCurrency].userAssetsSmallThreshold; const filteredAllIdsArray = allIdsArray.filter(id => { const asset = userAssetsMap.get(id); @@ -366,7 +366,8 @@ const storeManager = createRainbowStore( } ); -function getOrCreateStore(address: Address | string): UserAssetsStoreType { +function getOrCreateStore(): UserAssetsStoreType { + const address = reduxStore.getState().settings.accountAddress; const { stores } = storeManager.getState(); let store = stores.get(address); @@ -381,16 +382,16 @@ function getOrCreateStore(address: Address | string): UserAssetsStoreType { } export const userAssetsStore = { - getState: (address: Address | string) => getOrCreateStore(address).getState(), - setState: (address: Address | string, partial: Partial | ((state: UserAssetsState) => Partial)) => - getOrCreateStore(address).setState(partial), + getState: () => getOrCreateStore().getState(), + setState: (partial: Partial | ((state: UserAssetsState) => Partial)) => + getOrCreateStore().setState(partial), }; -export function useUserAssetsStore(address: Address | string, selector: (state: UserAssetsState) => T) { - const store = getOrCreateStore(address); - return useStore(store, useCallback(selector, [address])); +export function useUserAssetsStore(selector: (state: UserAssetsState) => T) { + const store = getOrCreateStore(); + return useStore(store, useCallback(selector, [])); } -function getCurrentSearchCache(address: Address | string): Map | undefined { - return getOrCreateStore(address).getState().searchCache; +function getCurrentSearchCache(): Map | undefined { + return getOrCreateStore().getState().searchCache; } diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index a9cc65121c9..1c0519278f0 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -26,9 +26,9 @@ export const UserAssetsSync = memo(function UserAssetsSync() { }), onSuccess: data => { if (!isSwapsOpen) { - userAssetsStore.getState(accountAddress).setUserAssets(data as ParsedSearchAsset[]); + userAssetsStore.getState().setUserAssets(data as ParsedSearchAsset[]); - const inputAsset = userAssetsStore.getState(accountAddress).getHighestValueEth(); + const inputAsset = userAssetsStore.getState().getHighestValueEth(); useSwapsStore.setState({ inputAsset, selectedOutputChainId: inputAsset?.chainId ?? ChainId.mainnet, From 5c802b062bba264efb68e1cec72e5753a4a77293 Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Mon, 26 Aug 2024 19:03:41 -0400 Subject: [PATCH 09/13] fix caching --- src/state/assets/userAssets.ts | 516 ++++++++++++++++----------------- 1 file changed, 248 insertions(+), 268 deletions(-) diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 353b4e8525d..c0b5df644ef 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -46,325 +46,305 @@ export interface UserAssetsState { setUserAssets: (userAssets: Map | ParsedSearchAsset[]) => void; } -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); - }, +// NOTE: We are serializing Map as an Array<[UniqueId, ParsedSearchAsset]> +type UserAssetsStateWithTransforms = Omit, 'chainBalances' | 'idsByChain' | 'userAssets'> & { + chainBalances: Array<[ChainId, number]>; + idsByChain: Array<[UserAssetFilter, UniqueId[]]>; + userAssets: Array<[UniqueId, ParsedSearchAsset]>; +}; - getChainsWithBalance: () => { - const chainBalances = [...get().chainBalances.entries()]; - const chainsWithBalances = chainBalances.filter(([, balance]) => !!balance); - return chainsWithBalances.map(([chainId]) => chainId); - }, +function serializeUserAssetsState(state: Partial, version?: number) { + try { + const transformedStateToPersist: UserAssetsStateWithTransforms = { + ...state, + chainBalances: state.chainBalances ? Array.from(state.chainBalances.entries()) : [], + idsByChain: state.idsByChain ? Array.from(state.idsByChain.entries()) : [], + userAssets: state.userAssets ? Array.from(state.userAssets.entries()) : [], + }; - 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(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; - } - }, + return JSON.stringify({ + state: transformedStateToPersist, + version, + }); + } catch (error) { + logger.error(new RainbowError(`[userAssetsStore]: Failed to serialize state for user assets storage`), { error }); + throw error; + } +} - getHighestValueEth: () => { - const preferredNetwork = swapsStore.getState().preferredNetwork; - const assets = get().userAssets; +function deserializeUserAssetsState(serializedState: string) { + let parsedState: { state: UserAssetsStateWithTransforms; version: number }; + try { + parsedState = JSON.parse(serializedState); + } catch (error) { + logger.error(new RainbowError(`[userAssetsStore]: Failed to parse serialized state from user assets storage`), { error }); + throw error; + } - let highestValueEth = null; + const { state, version } = parsedState; - for (const [, asset] of assets) { - if (asset.mainnetAddress !== ETH_ADDRESS) continue; + let chainBalances = new Map(); + try { + if (state.chainBalances) { + chainBalances = new Map(state.chainBalances); + } + } catch (error) { + logger.error(new RainbowError(`[userAssetsStore]: Failed to convert chainBalances from user assets storage`), { error }); + } - if (preferredNetwork && asset.chainId === preferredNetwork) { - return asset; - } + let idsByChain = new Map(); + try { + if (state.idsByChain) { + idsByChain = new Map(state.idsByChain); + } + } catch (error) { + logger.error(new RainbowError(`[userAssetsStore]: Failed to convert idsByChain from user assets storage`), { error }); + } - if (!highestValueEth || asset.balance > highestValueEth.balance) { - highestValueEth = asset; - } - } + let userAssetsData: Map = new Map(); + try { + if (state.userAssets.length) { + userAssetsData = new Map(state.userAssets); + } + } catch (error) { + logger.error(new RainbowError(`[userAssetsStore]: Failed to convert userAssets from user assets storage`), { error }); + } - return highestValueEth; + return { + state: { + ...state, + chainBalances, + idsByChain, + userAssets: userAssetsData, }, + version, + }; +} - getUserAsset: (uniqueId: UniqueId) => get().userAssets.get(uniqueId) || null, +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(); - getUserAssets: () => Array.from(get().userAssets.values()) || [], + const smallBalanceThreshold = supportedNativeCurrencies[reduxStore.getState().settings.nativeCurrency].userAssetsSmallThreshold; - selectUserAssetIds: function* (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) { - const { currentAbortController, idsByChain, userAssets } = get(); + 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(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; + } + }, - const assetIds = filter ? idsByChain.get(filter) || [] : idsByChain.get('all') || []; + getHighestValueEth: () => { + const preferredNetwork = swapsStore.getState().preferredNetwork; + const assets = get().userAssets; - for (const id of assetIds) { - if (currentAbortController?.signal.aborted) { - return; - } - const asset = userAssets.get(id); - if (asset && selector(asset)) { - yield id; - } - } - }, + let highestValueEth = null; - selectUserAssets: function* (selector: (asset: ParsedSearchAsset) => boolean) { - const { currentAbortController, userAssets } = get(); + for (const [, asset] of assets) { + if (asset.mainnetAddress !== ETH_ADDRESS) continue; - for (const [id, asset] of userAssets) { - if (currentAbortController?.signal.aborted) { - return; - } - if (selector(asset)) { - yield [id, asset]; + if (preferredNetwork && asset.chainId === preferredNetwork) { + return asset; + } + + if (!highestValueEth || asset.balance > highestValueEth.balance) { + highestValueEth = asset; + } } - } - }, - setSearchQuery: query => - set(state => { - const { currentAbortController } = state; + return highestValueEth; + }, - // Abort any ongoing search work - currentAbortController.abort(); + getUserAsset: (uniqueId: UniqueId) => get().userAssets.get(uniqueId) || null, - // Create a new AbortController for the new query - const abortController = new AbortController(); + getUserAssets: () => Array.from(get().userAssets.values()) || [], - return { inputSearchQuery: query.trim().toLowerCase(), currentAbortController: abortController }; - }), + selectUserAssetIds: function* (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) { + const { currentAbortController, idsByChain, userAssets } = get(); - setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => { - set(state => { - const newCache = new Map(state.searchCache).set(queryKey, filteredIds); + const assetIds = filter ? idsByChain.get(filter) || [] : idsByChain.get('all') || []; - // 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; - } + for (const id of assetIds) { + if (currentAbortController?.signal.aborted) { + return; + } + const asset = userAssets.get(id); + if (asset && selector(asset)) { + yield id; } } + }, - 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)); - }); + selectUserAssets: function* (selector: (asset: ParsedSearchAsset) => boolean) { + const { currentAbortController, userAssets } = get(); - // Ensure all supported chains are in the map with a fallback value of 0 - SUPPORTED_CHAIN_IDS({ testnetMode: false }).forEach(chainId => { - if (!unsortedChainBalances.has(chainId)) { - unsortedChainBalances.set(chainId, 0); - idsByChain.set(chainId, []); + for (const [id, asset] of userAssets) { + if (currentAbortController?.signal.aborted) { + return; } - }); - - // Sort the existing map by balance in descending order - const sortedEntries = Array.from(unsortedChainBalances.entries()).sort(([, balanceA], [, balanceB]) => balanceB - balanceA); - const chainBalances = new Map(); + if (selector(asset)) { + yield [id, asset]; + } + } + }, - sortedEntries.forEach(([chainId, balance]) => { - chainBalances.set(chainId, balance); - idsByChain.set(chainId, idsByChain.get(chainId) || []); - }); + setSearchQuery: query => + set(state => { + const { currentAbortController } = state; - 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])); + // Abort any ongoing search work + currentAbortController.abort(); - idsByChain.set('all', allIdsArray); + // Create a new AbortController for the new query + const abortController = new AbortController(); - const smallBalanceThreshold = supportedNativeCurrencies[reduxStore.getState().settings.nativeCurrency].userAssetsSmallThreshold; + return { inputSearchQuery: query.trim().toLowerCase(), currentAbortController: abortController }; + }), - const filteredAllIdsArray = allIdsArray.filter(id => { - const asset = userAssetsMap.get(id); - return asset && (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold; - }); + setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => { + set(state => { + const newCache = new Map(state.searchCache).set(queryKey, filteredIds); - const searchCache = new Map(); + // 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; + } + } + } - Array.from(chainBalances.keys()).forEach(userAssetFilter => { - const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); - searchCache.set(`${userAssetFilter}`, filteredIds); + 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: false }).forEach(chainId => { + if (!unsortedChainBalances.has(chainId)) { + unsortedChainBalances.set(chainId, 0); + idsByChain.set(chainId, []); + } + }); - searchCache.set('all', filteredAllIdsArray); + // Sort the existing map by balance in descending order + const sortedEntries = Array.from(unsortedChainBalances.entries()).sort(([, balanceA], [, balanceB]) => balanceB - balanceA); + const chainBalances = new Map(); - if (isMap) { - return { chainBalances, idsByChain, searchCache, userAssets }; - } else - return { - chainBalances, - idsByChain, - searchCache, - userAssets: userAssetsMap, - }; - }), - })); + sortedEntries.forEach(([chainId, balance]) => { + chainBalances.set(chainId, balance); + idsByChain.set(chainId, idsByChain.get(chainId) || []); + }); -type UserAssetsStoreType = ReturnType; + 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])); -interface StoreManagerState { - stores: Map
; -} + idsByChain.set('all', allIdsArray); -// NOTE: We are serializing Map as an Array<[UniqueId, ParsedSearchAsset]> -type UserAssetsStateWithTransforms = Omit, 'chainBalances' | 'idsByChain' | 'userAssets'> & { - chainBalances: Array<[ChainId, number]>; - idsByChain: Array<[UserAssetFilter, UniqueId[]]>; - userAssets: Array<[UniqueId, ParsedSearchAsset]>; -}; + const smallBalanceThreshold = supportedNativeCurrencies[reduxStore.getState().settings.nativeCurrency].userAssetsSmallThreshold; -type StoreManagerStateWithTransforms = { stores: Array<[Address | string, UserAssetsStateWithTransforms]> }; + const filteredAllIdsArray = allIdsArray.filter(id => { + const asset = userAssetsMap.get(id); + return asset && (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold; + }); -function serializeStoreManager(state: Partial, version?: number) { - try { - const transformedStateToPersist: StoreManagerStateWithTransforms = { - stores: state.stores - ? Array.from(state.stores.entries()).map(([address, store]) => { - const storeState = store.getState(); - const transformedStore = { - chainBalances: storeState.chainBalances ? Array.from(storeState.chainBalances.entries()) : [], - idsByChain: storeState.idsByChain ? Array.from(storeState.idsByChain.entries()) : [], - userAssets: storeState.userAssets ? Array.from(storeState.userAssets.entries()) : [], - }; - return [address, transformedStore]; - }) - : [], - }; + const searchCache = new Map(); - return JSON.stringify({ - state: transformedStateToPersist, - version, - }); - } catch (error) { - logger.error(new RainbowError('[userAssetsStore]: Failed to serialize state for user assets storage'), { error }); - throw error; - } -} + Array.from(chainBalances.keys()).forEach(userAssetFilter => { + const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); + searchCache.set(`${userAssetFilter}`, filteredIds); + }); -function deserializeStoreManager(serializedState: string) { - let parsedState: { state: StoreManagerStateWithTransforms; version: number }; - try { - parsedState = JSON.parse(serializedState); - } catch (error) { - logger.error(new RainbowError('[userAssetsStore]: Failed to parse serialized state from user assets storage'), { error }); - throw error; - } - - const { state, version } = parsedState; - - const stores = new Map
(); - - state.stores.forEach(([address, transformedStore]) => { - let chainBalances = new Map(); - try { - if (transformedStore.chainBalances) { - chainBalances = new Map(transformedStore.chainBalances); - } - } catch (error) { - logger.error(new RainbowError('[userAssetsStore]: Failed to convert chainBalances from user assets storage'), { error }); - } - - let idsByChain = new Map(); - try { - if (transformedStore.idsByChain) { - idsByChain = new Map(transformedStore.idsByChain); - } - } catch (error) { - logger.error(new RainbowError('[userAssetsStore]: Failed to convert idsByChain from user assets storage'), { error }); - } + searchCache.set('all', filteredAllIdsArray); - let userAssets: Map = new Map(); - try { - if (transformedStore.userAssets.length) { - userAssets = new Map(transformedStore.userAssets); - } - } catch (error) { - logger.error(new RainbowError('[userAssetsStore]: Failed to convert userAssets from user assets storage'), { error }); + if (isMap) { + return { chainBalances, idsByChain, searchCache, userAssets }; + } else + return { + chainBalances, + idsByChain, + searchCache, + userAssets: userAssetsMap, + }; + }), + }), + { + storageKey: `userAssets_${address}`, + version: 0, + serializer: serializeUserAssetsState, + deserializer: deserializeUserAssetsState, } + ); - const rehydratedStore = createUserAssetsStore(address); - - rehydratedStore.setState({ - chainBalances, - idsByChain, - userAssets, - }); - - stores.set(address, rehydratedStore); - }); +type UserAssetsStoreType = ReturnType; - return { - state: { - stores: stores, - }, - version, - }; +interface StoreManagerState { + stores: Map
; } -const storeManager = createRainbowStore( - () => ({ - stores: new Map(), - }), - { - storageKey: 'userAssetsStoreManager', - version: 1, - serializer: serializeStoreManager, - deserializer: deserializeStoreManager, - } -); +const storeManager = createRainbowStore(() => ({ + stores: new Map(), +})); function getOrCreateStore(): UserAssetsStoreType { const address = reduxStore.getState().settings.accountAddress; From 21918cf9938071c98d4296b41bebc002ee45cc0b Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Tue, 27 Aug 2024 10:18:37 -0400 Subject: [PATCH 10/13] select address --- src/state/assets/userAssets.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index ce1d3f6fd93..b7fdb811454 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -2,12 +2,13 @@ import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/ import { ChainId } from '@/__swaps__/types/chains'; import { Address } from 'viem'; import { RainbowError, logger } from '@/logger'; -import reduxStore 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'; const SEARCH_CACHE_MAX_ENTRIES = 50; @@ -345,15 +346,15 @@ const storeManager = createRainbowStore(() => ({ stores: new Map(), })); -function getOrCreateStore(): UserAssetsStoreType { - const address = reduxStore.getState().settings.accountAddress; +function getOrCreateStore(address?: Address | string): UserAssetsStoreType { + const accountAddress = address ?? reduxStore.getState().settings.accountAddress; const { stores } = storeManager.getState(); - let store = stores.get(address); + let store = stores.get(accountAddress); if (!store) { - store = createUserAssetsStore(address); + store = createUserAssetsStore(accountAddress); storeManager.setState(state => ({ - stores: new Map(state.stores).set(address, store as UserAssetsStoreType), + stores: new Map(state.stores).set(accountAddress, store as UserAssetsStoreType), })); } @@ -367,7 +368,8 @@ export const userAssetsStore = { }; export function useUserAssetsStore(selector: (state: UserAssetsState) => T) { - const store = getOrCreateStore(); + const address = useSelector((state: AppState) => state.settings.accountAddress); + const store = getOrCreateStore(address); return useStore(store, useCallback(selector, [])); } From 21a656a099e0564b4f15fb74f62a76b6a84f511f Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Tue, 27 Aug 2024 10:52:32 -0400 Subject: [PATCH 11/13] comments --- src/__swaps__/screens/Swap/providers/swap-provider.tsx | 4 ++-- src/state/sync/UserAssetsSync.tsx | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index b852eb423e3..198c9807b2c 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -651,8 +651,8 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { [ SwapInputController.quoteFetchingInterval, handleProgressNavigation, - internalSelectedInputAsset.value, - internalSelectedOutputAsset.value, + internalSelectedInputAsset, + internalSelectedOutputAsset, selectedOutputChainId, updateAssetValue, ] diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index 7baca34601b..f30df075fd4 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -1,4 +1,3 @@ -import { memo } from 'react'; import { useAccountSettings } from '@/hooks'; import { userAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; @@ -8,7 +7,7 @@ import { ChainId } from '@/__swaps__/types/chains'; import { getIsHardhatConnected } from '@/handlers/web3'; import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; -export const UserAssetsSync = memo(function UserAssetsSync() { +export const UserAssetsSync = function UserAssetsSync() { const { accountAddress, nativeCurrency: currentCurrency } = useAccountSettings(); const isSwapsOpen = useSwapsStore(state => state.isSwapsOpen); @@ -41,4 +40,4 @@ export const UserAssetsSync = memo(function UserAssetsSync() { ); return null; -}); +}; From 65951fe3cbaa6cce1badd116109f0a7668f9f230 Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Thu, 5 Sep 2024 10:28:43 -0400 Subject: [PATCH 12/13] hardhat --- src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts | 4 ---- src/__swaps__/screens/Swap/resources/assets/userAssets.ts | 7 +++++-- src/state/sync/UserAssetsSync.tsx | 3 --- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index 236f907719d..e78faa661c4 100644 --- a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts +++ b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts @@ -9,7 +9,6 @@ import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; import { ParsedAssetsDictByChain, ParsedSearchAsset, UserAssetFilter } from '@/__swaps__/types/assets'; import { useAccountSettings, useDebounce } from '@/hooks'; import { useUserAssetsStore } from '@/state/assets/userAssets'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const sortBy = (by: UserAssetFilter) => { switch (by) { @@ -30,13 +29,10 @@ export const useAssetsToSell = () => { const debouncedAssetToSellFilter = useDebounce(searchQuery, 200); - const { connectedToHardhat } = useConnectedToHardhatStore(); - const { data: userAssets = [] } = useUserAssets( { address: currentAddress, currency: currentCurrency, - testnetMode: connectedToHardhat, }, { select: data => diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 4261e549ef5..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', @@ -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/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index 84264df3adb..139d4ddb547 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -5,19 +5,16 @@ 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, nativeCurrency: currentCurrency } = useAccountSettings(); const isSwapsOpen = useSwapsStore(state => state.isSwapsOpen); - const { connectedToHardhat } = useConnectedToHardhatStore(); useUserAssets( { address: accountAddress, currency: currentCurrency, - testnetMode: connectedToHardhat, }, { enabled: !isSwapsOpen, From 20a73a54ddbe3813027efc1d35b8e5bbeda65e1f Mon Sep 17 00:00:00 2001 From: Ben Goldberg Date: Tue, 10 Sep 2024 10:11:10 -0400 Subject: [PATCH 13/13] lint --- src/components/MobileWalletProtocolListener.tsx | 1 - 1 file changed, 1 deletion(-) 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