diff --git a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx index beee83e8b3e..715ff149768 100644 --- a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx +++ b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx @@ -15,7 +15,6 @@ import { import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; import { ChainId } from '@/state/backendNetworks/types'; import { ParsedAddressAsset } from '@/entities'; -import { useUserNativeNetworkAsset } from '@/resources/assets/useUserAsset'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { deepEqualWorklet } from '@/worklets/comparisons'; import { debounce } from 'lodash'; @@ -26,7 +25,10 @@ import { GasSettings } from '../hooks/useCustomGas'; import { useSelectedGas } from '../hooks/useSelectedGas'; import { useSwapEstimatedGasLimit } from '../hooks/useSwapEstimatedGasLimit'; import { useSwapContext } from './swap-provider'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; +import { getUniqueId } from '@/utils/ethereumUtils'; import { useSwapsStore } from '@/state/swaps/swapsStore'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const BUFFER_RATIO = 0.5; @@ -143,7 +145,12 @@ export function SyncGasStateToSharedValues() { const { assetToSell, chainId = initialChainId, quote } = useSyncedSwapQuoteStore(); const gasSettings = useSelectedGas(chainId); - const { data: userNativeNetworkAsset, isLoading: isLoadingNativeNetworkAsset } = useUserNativeNetworkAsset(chainId); + + const { userNativeNetworkAsset, isLoadingNativeNetworkAsset } = useUserAssetsStore(state => { + const { address: nativeCurrencyAddress } = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]; + const uniqueId = getUniqueId(nativeCurrencyAddress, chainId); + return { userNativeNetworkAsset: state.getLegacyUserAsset(uniqueId), isLoadingNativeNetworkAsset: state.isLoadingUserAssets }; + }); const { data: estimatedGasLimit } = useSwapEstimatedGasLimit({ chainId, assetToSell, quote }); const gasFeeRange = useSharedValue<[string, string] | null>(null); diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index da16527c726..4d1f66ae1c4 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -22,7 +22,7 @@ import { NavigationSteps, useSwapNavigation } from '@/__swaps__/screens/Swap/hoo import { useSwapSettings } from '@/__swaps__/screens/Swap/hooks/useSwapSettings'; import { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextStyles'; import { SwapWarningType, useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; -import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; +import { userAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; import { ChainId } from '@/state/backendNetworks/types'; import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; @@ -41,7 +41,6 @@ import Routes from '@/navigation/routesNames'; 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 { swapsStore } from '@/state/swaps/swapsStore'; import { getNextNonce } from '@/state/nonces'; @@ -342,14 +341,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { userAssetsQueryKey({ address: parameters.quote.from, currency: nativeCurrency, - connectedToHardhat, - }) - ); - queryClient.invalidateQueries( - swapsUserAssetsQueryKey({ - address: parameters.quote.from as Address, - currency: nativeCurrency, - testnetMode: !!connectedToHardhat, + testnetMode: connectedToHardhat, }) ); diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 76e685a1545..6c2e0611f70 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -17,6 +17,8 @@ import { fetchUserAssetsByChain } from './userAssetsByChain'; import { fetchHardhatBalancesByChainId } from '@/resources/assets/hardhatAssets'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { staleBalancesStore } from '@/state/staleBalances'; +import { IS_TEST } from '@/env'; import store from '@/redux/store'; const addysHttp = new RainbowFetchClient({ @@ -28,7 +30,7 @@ const addysHttp = new RainbowFetchClient({ const USER_ASSETS_REFETCH_INTERVAL = 60000; const USER_ASSETS_TIMEOUT_DURATION = 20000; -export const USER_ASSETS_STALE_INTERVAL = 30000; +const USER_ASSETS_STALE_INTERVAL = 30000; // /////////////////////////////////////////////// // Query Types @@ -123,20 +125,23 @@ async function userAssetsQueryFunction({ return parsedAssetsDict; } + const cache = queryClient.getQueryCache(); const cachedUserAssets = (cache.find(userAssetsQueryKey({ address, currency, testnetMode }))?.state?.data || {}) as ParsedAssetsDictByChain; try { - const url = `/${useBackendNetworksStore.getState().getSupportedChainIds().join(',')}/${address}/assets`; + staleBalancesStore.getState().clearExpiredData(address); + const staleBalanceParam = staleBalancesStore.getState().getStaleBalancesQueryParam(address); + let url = `/${useBackendNetworksStore.getState().getSupportedChainIds().join(',')}/${address}/assets?currency=${currency.toLowerCase()}`; + if (staleBalanceParam) { + url += staleBalanceParam; + } const res = await addysHttp.get(url, { - params: { - currency: currency.toLowerCase(), - }, timeout: USER_ASSETS_TIMEOUT_DURATION, }); const chainIdsInResponse = res?.data?.meta?.chain_ids || []; const chainIdsWithErrorsInResponse = res?.data?.meta?.chain_ids_with_errors || []; - const assets = res?.data?.payload?.assets || []; + const assets = res?.data?.payload?.assets?.filter(asset => !asset.asset.defi_position) || []; if (address) { if (chainIdsWithErrorsInResponse.length) { userAssetsQueryFunctionRetryByChain({ @@ -170,7 +175,7 @@ async function userAssetsQueryFunction({ } } -type UserAssetsResult = QueryFunctionResult; +export type UserAssetsResult = QueryFunctionResult; async function userAssetsQueryFunctionRetryByChain({ address, @@ -258,6 +263,6 @@ export function useUserAssets( ...config, enabled: !!address && !!currency, refetchInterval: USER_ASSETS_REFETCH_INTERVAL, - staleTime: process.env.IS_TESTING === 'true' ? 0 : 1000, + staleTime: IS_TEST ? 0 : USER_ASSETS_STALE_INTERVAL, }); } diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts index 723334cce51..032b5ef2eff 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts @@ -70,7 +70,7 @@ async function userAssetsByChainQueryFunction({ }, }); const chainIdsInResponse = res?.data?.meta?.chain_ids || []; - const assets = res?.data?.payload?.assets || []; + const assets = res?.data?.payload?.assets?.filter(asset => !asset.asset.defi_position) || []; if (assets.length && chainIdsInResponse.length) { const parsedAssetsDict = await parseUserAssets({ assets, diff --git a/src/__swaps__/types/assets.ts b/src/__swaps__/types/assets.ts index c6f7bcd84a5..f473d907900 100644 --- a/src/__swaps__/types/assets.ts +++ b/src/__swaps__/types/assets.ts @@ -140,6 +140,7 @@ export interface ZerionAsset { bridgeable: boolean; networks: { [id in ChainId]?: { bridgeable: boolean } }; }; + defi_position?: boolean; } // protocols https://github.com/rainbow-me/go-utils-lib/blob/master/pkg/enums/token_type.go#L44 diff --git a/src/__swaps__/utils/assets.ts b/src/__swaps__/utils/assets.ts index c2fa5dfd935..8c87ad572bb 100644 --- a/src/__swaps__/utils/assets.ts +++ b/src/__swaps__/utils/assets.ts @@ -234,37 +234,6 @@ export function parseUserAssetBalances({ }; } -export function parseParsedUserAsset({ - parsedAsset, - currency, - quantity, -}: { - parsedAsset: ParsedUserAsset; - currency: SupportedCurrencyKey; - quantity: string; -}): ParsedUserAsset { - const amount = convertRawAmountToDecimalFormat(quantity, parsedAsset?.decimals); - return { - ...parsedAsset, - balance: { - amount, - display: convertAmountToBalanceDisplay(amount, { - decimals: parsedAsset?.decimals, - symbol: parsedAsset?.symbol, - }), - }, - native: { - ...parsedAsset.native, - balance: getNativeAssetBalance({ - currency, - decimals: parsedAsset?.decimals, - priceUnit: parsedAsset?.price?.value || 0, - value: amount, - }), - }, - }; -} - export const parseSearchAsset = ({ assetWithPrice, searchAsset, diff --git a/src/components/asset-list/AssetListHeader.js b/src/components/asset-list/AssetListHeader.js index d35ee0bd3a8..7e1013861be 100644 --- a/src/components/asset-list/AssetListHeader.js +++ b/src/components/asset-list/AssetListHeader.js @@ -12,11 +12,11 @@ import { StickyHeader } from './RecyclerAssetList2/core/StickyHeaders'; import { useAccountProfile, useDimensions } from '@/hooks'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import { useUserAssetCount } from '@/resources/assets/useUserAssetCount'; import styled from '@/styled-thing'; import { fonts, position } from '@/styles'; import { useTheme } from '@/theme'; import * as lang from '@/languages'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; export const AssetListHeaderHeight = ListHeaderHeight; @@ -104,7 +104,7 @@ const AssetListHeader = ({ contextMenuOptions, isCoinListEdited, title, totalVal const { width: deviceWidth } = useDimensions(); const { accountName } = useAccountProfile(); const { navigate } = useNavigation(); - const { isLoading: isLoadingUserAssets } = useUserAssetCount(); + const { isLoadingUserAssets } = useUserAssetsStore(state => state.isLoadingUserAssets); const onChangeWallet = useCallback(() => { navigate(Routes.CHANGE_WALLET_SHEET); diff --git a/src/components/list/NoResults.tsx b/src/components/list/NoResults.tsx index ef610f7b36e..ff9b3789bd7 100644 --- a/src/components/list/NoResults.tsx +++ b/src/components/list/NoResults.tsx @@ -4,7 +4,7 @@ import { neverRerender } from '@/utils'; import { Inset, Stack, Text } from '@/design-system'; import { useTheme } from '@/theme'; import { logger } from '@/logger'; -import { useUserAssetCount } from '@/resources/assets/useUserAssetCount'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; export enum NoResultsType { Discover = 'discover', @@ -14,7 +14,7 @@ export enum NoResultsType { export const NoResults = ({ onL2, type }: { onL2?: boolean; type: NoResultsType }) => { const { colors } = useTheme(); - const { data: assetCount } = useUserAssetCount(); + const assetCount = useUserAssetsStore(state => state.userAssets.size); let title; let description; diff --git a/src/components/remote-promo-sheet/check-fns/hasNonZeroTotalBalance.ts b/src/components/remote-promo-sheet/check-fns/hasNonZeroTotalBalance.ts index 2d0976b21df..e2f68b89723 100644 --- a/src/components/remote-promo-sheet/check-fns/hasNonZeroTotalBalance.ts +++ b/src/components/remote-promo-sheet/check-fns/hasNonZeroTotalBalance.ts @@ -1,16 +1,20 @@ +import { selectorFilterByUserChains, selectUserAssetsList } from '@/__swaps__/screens/Swap/resources/_selectors/assets'; +import { userAssetsFetchQuery } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import store from '@/redux/store'; -import { fetchUserAssets } from '@/resources/assets/UserAssetsQuery'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; export const hasNonZeroTotalBalance = async (): Promise => { const { accountAddress, nativeCurrency } = store.getState().settings; - const assets = await fetchUserAssets({ + const userAssetsDictByChain = await userAssetsFetchQuery({ address: accountAddress, currency: nativeCurrency, - connectedToHardhat: false, + testnetMode: useConnectedToHardhatStore.getState().connectedToHardhat, }); - if (!assets || Object.keys(assets).length === 0) return false; + const assets = selectorFilterByUserChains({ data: userAssetsDictByChain, selector: selectUserAssetsList }); - return Object.values(assets).some(asset => Number(asset.balance?.amount) > 0); + if (!assets?.length) return false; + + return assets.some(asset => Number(asset.balance?.amount) > 0); }; diff --git a/src/helpers/buildWalletSections.tsx b/src/helpers/buildWalletSections.tsx index b498081c0f5..b7ad6089c6c 100644 --- a/src/helpers/buildWalletSections.tsx +++ b/src/helpers/buildWalletSections.tsx @@ -223,7 +223,7 @@ const withBriefBalanceSection = ( type: 'PROFILE_NAME_ROW_SPACE_AFTER', uid: 'profile-name-space-after', }, - ...(!hasTokens && !isLoadingUserAssets && !isLoadingBalance + ...(!hasTokens && !isLoadingBalance ? [] : [ { diff --git a/src/hooks/useAccountAsset.ts b/src/hooks/useAccountAsset.ts index 2d2bccc2699..0e5ba4a00e3 100644 --- a/src/hooks/useAccountAsset.ts +++ b/src/hooks/useAccountAsset.ts @@ -1,11 +1,11 @@ import { NativeCurrencyKey } from '@/entities'; import useAccountSettings from './useAccountSettings'; import { parseAssetNative } from '@/parsers'; -import { useUserAsset } from '@/resources/assets/useUserAsset'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; // this is meant to be used for assets contained in the current wallet export default function useAccountAsset(uniqueId: string, nativeCurrency: NativeCurrencyKey | undefined = undefined) { - const { data: accountAsset } = useUserAsset(uniqueId); + const accountAsset = useUserAssetsStore(state => state.getLegacyUserAsset(uniqueId)); // this is temporary for FastBalanceCoinRow to make a tiny bit faster // we pass nativeCurrency only in that case diff --git a/src/hooks/useColorForAsset.ts b/src/hooks/useColorForAsset.ts index bfaf954e388..21eab85420b 100644 --- a/src/hooks/useColorForAsset.ts +++ b/src/hooks/useColorForAsset.ts @@ -19,6 +19,10 @@ export default function useColorForAsset( const isDarkMode = forceLightMode || isDarkModeTheme; const colorDerivedFromAddress = useMemo(() => { + if (!resolvedAddress) { + return undefined; + } + const color = isETH(resolvedAddress) ? isDarkMode ? forceETHColor diff --git a/src/hooks/useRefreshAccountData.ts b/src/hooks/useRefreshAccountData.ts index 388dfc2fcc3..fd2ffbb4b87 100644 --- a/src/hooks/useRefreshAccountData.ts +++ b/src/hooks/useRefreshAccountData.ts @@ -6,8 +6,7 @@ import useAccountSettings from './useAccountSettings'; import { PROFILES, useExperimentalFlag } from '@/config'; import { logger, RainbowError } from '@/logger'; import { queryClient } from '@/react-query'; -import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; -import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; +import { userAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { invalidateAddressNftsQueries } from '@/resources/nfts'; import { positionsQueryKey } from '@/resources/defi/PositionsQuery'; import { Address } from 'viem'; @@ -35,9 +34,8 @@ export default function useRefreshAccountData() { queryClient.invalidateQueries(positionsQueryKey({ address: accountAddress as Address, currency: nativeCurrency })); queryClient.invalidateQueries(claimablesQueryKey({ address: accountAddress, currency: nativeCurrency })); queryClient.invalidateQueries(addysSummaryQueryKey({ addresses: allAddresses, currency: nativeCurrency })); - queryClient.invalidateQueries(userAssetsQueryKey({ address: accountAddress, currency: nativeCurrency, connectedToHardhat })); queryClient.invalidateQueries( - swapsUserAssetsQueryKey({ address: accountAddress as Address, currency: nativeCurrency, testnetMode: !!connectedToHardhat }) + userAssetsQueryKey({ address: accountAddress, currency: nativeCurrency, testnetMode: connectedToHardhat }) ); try { diff --git a/src/hooks/useWalletSectionsData.ts b/src/hooks/useWalletSectionsData.ts index db186edc714..a8f6072f1ea 100644 --- a/src/hooks/useWalletSectionsData.ts +++ b/src/hooks/useWalletSectionsData.ts @@ -9,14 +9,13 @@ import useSendableUniqueTokens from './useSendableUniqueTokens'; import useShowcaseTokens from './useShowcaseTokens'; import useWallets from './useWallets'; import { buildBriefWalletSectionsSelector } from '@/helpers/buildWalletSections'; -import { useSortedUserAssets } from '@/resources/assets/useSortedUserAssets'; import { useLegacyNFTs } from '@/resources/nfts'; import useWalletsWithBalancesAndNames from './useWalletsWithBalancesAndNames'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; import { useRemoteConfig } from '@/model/remoteConfig'; import { usePositions } from '@/resources/defi/PositionsQuery'; import { useClaimables } from '@/resources/addys/claimables/query'; import { useExperimentalConfig } from '@/config/experimentalHooks'; -import { useUserAssetsStore } from '@/state/assets/userAssets'; import { analyticsV2 } from '@/analytics'; import { Claimable } from '@/resources/addys/claimables/types'; import { throttle } from 'lodash'; @@ -55,13 +54,16 @@ export default function useWalletSectionsData({ }: { type?: string; } = {}) { + const { accountAddress, language, network, nativeCurrency } = useAccountSettings(); const { selectedWallet, isReadOnlyWallet } = useWallets(); - const { isLoading: isLoadingUserAssets, data: sortedAssets = [] } = useSortedUserAssets(); + const { isLoadingUserAssets, sortedAssets = [] } = useUserAssetsStore(state => ({ + sortedAssets: state.legacyUserAssets, + isLoadingUserAssets: state.isLoadingUserAssets, + })); const isWalletEthZero = useIsWalletEthZero(); const { nftSort, nftSortDirection } = useNftSort(); - const { accountAddress, language, network, nativeCurrency } = useAccountSettings(); const { sendableUniqueTokens } = useSendableUniqueTokens(); const { data: { nfts: allUniqueTokens }, diff --git a/src/hooks/useWalletsHiddenBalances.ts b/src/hooks/useWalletsHiddenBalances.ts index d12b3292f22..fccff352bd9 100644 --- a/src/hooks/useWalletsHiddenBalances.ts +++ b/src/hooks/useWalletsHiddenBalances.ts @@ -1,12 +1,8 @@ import { AllRainbowWallets } from '@/model/wallet'; import { useMemo, useState, useEffect, useCallback } from 'react'; import { Address } from 'viem'; -import useAccountSettings from './useAccountSettings'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -import { NativeCurrencyKey } from '@/entities/nativeCurrencyTypes'; import { userAssetsStore } from '@/state/assets/userAssets'; import { queryClient } from '@/react-query'; -import { userAssetsQueryKey, UserAssetsResult } from '@/resources/assets/UserAssetsQuery'; import { add, isEqual, multiply } from '@/helpers/utilities'; import { isEqual as _isEqual } from 'lodash'; @@ -14,22 +10,11 @@ export type WalletBalanceResult = { hiddenBalances: Record; }; -const getHiddenAssetBalance = ({ - address, - nativeCurrency, - connectedToHardhat, -}: { - address: Address; - nativeCurrency: NativeCurrencyKey; - connectedToHardhat: boolean; -}) => { +const getHiddenAssetBalance = ({ address }: { address: Address }) => { const hiddenAssetIds = userAssetsStore.getState(address).getHiddenAssetsIds(); - const assetData = queryClient.getQueryData( - userAssetsQueryKey({ address, currency: nativeCurrency, connectedToHardhat }) - ); const balance = hiddenAssetIds.reduce((acc, uniqueId) => { - const asset = assetData?.[uniqueId]; + const asset = userAssetsStore.getState(address).getUserAsset(uniqueId); if (!asset) return acc; const assetNativeBalance = multiply(asset.price?.value || 0, asset.balance?.amount || 0); return add(acc, assetNativeBalance); @@ -39,8 +24,6 @@ const getHiddenAssetBalance = ({ }; const useWalletsHiddenBalances = (wallets: AllRainbowWallets): WalletBalanceResult => { - const { nativeCurrency } = useAccountSettings(); - const connectedToHardhat = useConnectedToHardhatStore(state => state.connectedToHardhat); const [hiddenBalances, setHiddenBalances] = useState>({}); const allAddresses = useMemo( @@ -48,24 +31,21 @@ const useWalletsHiddenBalances = (wallets: AllRainbowWallets): WalletBalanceResu [wallets] ); - const calculateHiddenBalanceForAddress = useCallback( - (address: Address) => { - const lowerCaseAddress = address.toLowerCase() as Address; - const hiddenAssetBalance = getHiddenAssetBalance({ address, nativeCurrency, connectedToHardhat }); + const calculateHiddenBalanceForAddress = useCallback((address: Address) => { + const lowerCaseAddress = address.toLowerCase() as Address; + const hiddenAssetBalance = getHiddenAssetBalance({ address }); - setHiddenBalances(prev => { - const newBalance = hiddenAssetBalance; - if (!prev[lowerCaseAddress] || !isEqual(prev[lowerCaseAddress], newBalance)) { - return { - ...prev, - [lowerCaseAddress]: newBalance, - }; - } - return prev; - }); - }, - [nativeCurrency, connectedToHardhat] - ); + setHiddenBalances(prev => { + const newBalance = hiddenAssetBalance; + if (!prev[lowerCaseAddress] || !isEqual(prev[lowerCaseAddress], newBalance)) { + return { + ...prev, + [lowerCaseAddress]: newBalance, + }; + } + return prev; + }); + }, []); useEffect(() => { allAddresses.forEach(address => { @@ -97,7 +77,7 @@ const useWalletsHiddenBalances = (wallets: AllRainbowWallets): WalletBalanceResu subscriptions.forEach(sub => sub()); unsubscribeFromQueryCache(); }; - }, [allAddresses, calculateHiddenBalanceForAddress, connectedToHardhat, nativeCurrency]); + }, [allAddresses, calculateHiddenBalanceForAddress]); return { hiddenBalances }; }; diff --git a/src/hooks/useWatchPendingTxs.ts b/src/hooks/useWatchPendingTxs.ts index f939d6e408b..517185d1407 100644 --- a/src/hooks/useWatchPendingTxs.ts +++ b/src/hooks/useWatchPendingTxs.ts @@ -1,8 +1,7 @@ import { useMemo, useCallback } from 'react'; import useAccountSettings from './useAccountSettings'; +import { userAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { RainbowTransaction, MinedTransaction, TransactionStatus } from '@/entities'; -import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; -import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { transactionFetchQuery } from '@/resources/transactions/transaction'; import { RainbowError, logger } from '@/logger'; import { consolidatedTransactionsQueryKey } from '@/resources/transactions/consolidatedTransactions'; @@ -32,14 +31,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => userAssetsQueryKey({ address, currency: nativeCurrency, - connectedToHardhat, - }) - ); - queryClient.invalidateQueries( - swapsUserAssetsQueryKey({ - address: address as Address, - currency: nativeCurrency, - testnetMode: !!connectedToHardhat, + testnetMode: connectedToHardhat, }) ); invalidateAddressNftsQueries(address); @@ -123,7 +115,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => }); queryClient.refetchQueries({ - queryKey: userAssetsQueryKey({ address, currency: nativeCurrency, connectedToHardhat }), + queryKey: userAssetsQueryKey({ address, currency: nativeCurrency, testnetMode: connectedToHardhat }), }); const supportedMainnetChainIds = useBackendNetworksStore.getState().getSupportedMainnetChainIds(); diff --git a/src/model/migrations.ts b/src/model/migrations.ts index 68625a0686a..bfd41f7b887 100644 --- a/src/model/migrations.ts +++ b/src/model/migrations.ts @@ -39,8 +39,11 @@ import { EthereumAddress, RainbowToken } from '@/entities'; import { standardizeUrl, useFavoriteDappsStore } from '@/state/browser/favoriteDappsStore'; import { useLegacyFavoriteDappsStore } from '@/state/legacyFavoriteDapps'; import { getAddressAndChainIdFromUniqueId, getUniqueId, getUniqueIdNetwork } from '@/utils/ethereumUtils'; -import { UniqueId } from '@/__swaps__/types/assets'; +import { ParsedAssetsDictByChain, ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets'; import { userAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { selectorFilterByUserChains, selectUserAssetsList } from '@/__swaps__/screens/Swap/resources/_selectors/assets'; import { UnlockableAppIconKey, unlockableAppIcons } from '@/appIcons/appIcons'; import { unlockableAppIconStorage } from '@/featuresToUnlock/unlockableAppIconCheck'; @@ -712,6 +715,36 @@ export default async function runMigrations() { migrations.push(v22); + /** + *************** Migration v23 ****************** + * Populate `legacyUserAssets` attribute in `userAssetsStore` + */ + const v23 = async () => { + const state = store.getState(); + const { wallets } = state.wallets; + const { nativeCurrency } = state.settings; + + if (!wallets) return; + + for (const wallet of Object.values(wallets)) { + for (const { address } of (wallet as RainbowWallet).addresses) { + const { connectedToHardhat } = useConnectedToHardhatStore.getState(); + const queryKey = userAssetsQueryKey({ address, currency: nativeCurrency, testnetMode: connectedToHardhat }); + const queryData: ParsedAssetsDictByChain | undefined = queryClient.getQueryData(queryKey); + + if (!queryData) continue; + + const userAssets = selectorFilterByUserChains({ + data: queryData, + selector: selectUserAssetsList, + }); + userAssetsStore.getState(address).setUserAssets(userAssets as ParsedSearchAsset[]); + } + } + }; + + migrations.push(v23); + logger.debug(`[runMigrations]: ready to run migrations starting on number ${currentVersion}`); // await setMigrationVersion(17); if (migrations.length === currentVersion) { diff --git a/src/resources/assets/UserAssetsQuery.ts b/src/resources/assets/UserAssetsQuery.ts deleted file mode 100644 index 5616163e903..00000000000 --- a/src/resources/assets/UserAssetsQuery.ts +++ /dev/null @@ -1,222 +0,0 @@ -import isEmpty from 'lodash/isEmpty'; -import { ADDYS_API_KEY } from 'react-native-dotenv'; -import { NativeCurrencyKey } from '@/entities'; -import { saveAccountEmptyState } from '@/handlers/localstorage/accountLocal'; -import { greaterThan } from '@/helpers/utilities'; -import { rainbowFetch } from '@/rainbow-fetch'; -import { createQueryKey, queryClient, QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult } from '@/react-query'; -import { useQuery } from '@tanstack/react-query'; -import { filterPositionsData, parseAddressAsset } from './assets'; -import { fetchHardhatBalances } from './hardhatAssets'; -import { AddysAccountAssetsMeta, AddysAccountAssetsResponse, RainbowAddressAssets } from './types'; -import { Network } from '@/state/backendNetworks/types'; -import { staleBalancesStore } from '@/state/staleBalances'; -import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; - -// /////////////////////////////////////////////// -// Query Types - -export type UserAssetsArgs = { - address: string; // Address; - currency: NativeCurrencyKey; - connectedToHardhat: boolean; -}; - -// /////////////////////////////////////////////// -// Query Key - -export const userAssetsQueryKey = ({ address, currency, connectedToHardhat }: UserAssetsArgs) => - createQueryKey('userAssets', { address, currency, connectedToHardhat }, { persisterVersion: 1 }); - -type UserAssetsQueryKey = ReturnType; - -// /////////////////////////////////////////////// -// Query Function - -const fetchUserAssetsForChainIds = async ({ - address, - currency, - chainIds, - staleBalanceParam, -}: { - address: string; - currency: NativeCurrencyKey; - chainIds: number[]; - staleBalanceParam?: string; -}) => { - const chainIdsString = chainIds.join(','); - let url = `https://addys.p.rainbow.me/v3/${chainIdsString}/${address}/assets?currency=${currency.toLowerCase()}`; - - if (staleBalanceParam) { - url = url + staleBalanceParam; - } - - const response = await rainbowFetch(url, { - method: 'get', - headers: { - Authorization: `Bearer ${ADDYS_API_KEY}`, - }, - }); - - return response.data; -}; - -async function userAssetsQueryFunction({ - queryKey: [{ address, currency, connectedToHardhat }], -}: QueryFunctionArgs) { - const cache = queryClient.getQueryCache(); - const cachedAddressAssets = (cache.find(userAssetsQueryKey({ address, currency, connectedToHardhat }))?.state?.data || - {}) as RainbowAddressAssets; - - if (connectedToHardhat) { - const parsedHardhatResults = await fetchHardhatBalances(address); - return parsedHardhatResults; - } - - try { - staleBalancesStore.getState().clearExpiredData(address); - const staleBalanceParam = staleBalancesStore.getState().getStaleBalancesQueryParam(address); - - const { erroredChainIds, results } = await fetchAndParseUserAssetsForChainIds({ - address, - currency, - chainIds: useBackendNetworksStore.getState().getSupportedMainnetChainIds(), - staleBalanceParam, - }); - let parsedSuccessResults = results; - - // grab cached data for chain IDs with errors - if (!isEmpty(erroredChainIds)) { - const cachedDataForErroredChainIds = Object.keys(cachedAddressAssets) - .filter(uniqueId => { - const cachedAsset = cachedAddressAssets[uniqueId]; - return erroredChainIds?.find((chainId: number) => chainId === cachedAsset.chainId); - }) - .reduce((cur, uniqueId) => { - return Object.assign(cur, { - [uniqueId]: cachedAddressAssets[uniqueId], - }); - }, {}); - - parsedSuccessResults = { - ...parsedSuccessResults, - ...cachedDataForErroredChainIds, - }; - - retryErroredChainIds(address, currency, connectedToHardhat, erroredChainIds); - } - - return parsedSuccessResults; - } catch (e) { - return cachedAddressAssets; - } -} - -const retryErroredChainIds = async ( - address: string, - currency: NativeCurrencyKey, - connectedToHardhat: boolean, - erroredChainIds: number[] -) => { - const { meta, results } = await fetchAndParseUserAssetsForChainIds({ address, currency, chainIds: erroredChainIds }); - let parsedSuccessResults = results; - const successChainIds = meta?.chain_ids; - - if (isEmpty(successChainIds)) { - return; - } - - // grab cached data without data that will be replaced - const cache = queryClient.getQueryCache(); - const cachedAddressAssets = (cache.find(userAssetsQueryKey({ address, currency, connectedToHardhat }))?.state?.data || - {}) as RainbowAddressAssets; - - const cachedData = Object.keys(cachedAddressAssets) - .filter(uniqueId => { - const cachedAsset = cachedAddressAssets[uniqueId]; - return successChainIds?.find((chainId: number) => chainId !== cachedAsset.chainId); - }) - .reduce((cur, uniqueId) => { - return Object.assign(cur, { - [uniqueId]: cachedAddressAssets[uniqueId], - }); - }, {}); - - parsedSuccessResults = { - ...cachedData, - ...parsedSuccessResults, - }; - - queryClient.setQueryData(userAssetsQueryKey({ address, currency, connectedToHardhat }), parsedSuccessResults); -}; - -export type UserAssetsResult = QueryFunctionResult; - -interface AssetsAndMetadata { - erroredChainIds: number[]; - meta: AddysAccountAssetsMeta; - results: RainbowAddressAssets; -} - -const fetchAndParseUserAssetsForChainIds = async ({ - address, - currency, - chainIds, - staleBalanceParam, -}: { - address: string; - currency: NativeCurrencyKey; - chainIds: number[]; - staleBalanceParam?: string; -}): Promise => { - const data = await fetchUserAssetsForChainIds({ address, currency, chainIds, staleBalanceParam }); - let parsedSuccessResults = parseUserAssetsByChain(data); - - // filter out positions data - parsedSuccessResults = filterPositionsData(address, currency, parsedSuccessResults); - - // update account empty state - if (!isEmpty(parsedSuccessResults)) { - saveAccountEmptyState(false, address, Network.mainnet); - } - - const erroredChainIds = data?.meta?.chain_ids_with_errors; - return { erroredChainIds, meta: data?.meta, results: parsedSuccessResults }; -}; - -function parseUserAssetsByChain(message: AddysAccountAssetsResponse) { - return Object.values(message?.payload?.assets || {}).reduce((dict, assetData) => { - if (greaterThan(assetData?.quantity, 0)) { - const parsedAsset = parseAddressAsset({ - assetData, - }); - dict[parsedAsset?.uniqueId] = parsedAsset; - } - return dict; - }, {} as RainbowAddressAssets); -} - -// /////////////////////////////////////////////// -// Query Fetcher (Optional) - -export async function fetchUserAssets( - { address, currency, connectedToHardhat }: UserAssetsArgs, - config: QueryConfigWithSelect = {} -) { - return await queryClient.fetchQuery(userAssetsQueryKey({ address, currency, connectedToHardhat }), userAssetsQueryFunction, config); -} - -// /////////////////////////////////////////////// -// Query Hook - -export function useUserAssets( - { address, currency, connectedToHardhat }: UserAssetsArgs, - config: QueryConfigWithSelect = {} -) { - return useQuery(userAssetsQueryKey({ address, currency, connectedToHardhat }), userAssetsQueryFunction, { - enabled: !!address && !!currency, - staleTime: 60_000, // 1 minute - refetchInterval: 120_000, // 2 minutes - ...config, - }); -} diff --git a/src/resources/assets/assetSelectors.ts b/src/resources/assets/assetSelectors.ts deleted file mode 100644 index 41d6d2ba9e5..00000000000 --- a/src/resources/assets/assetSelectors.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; -import { parseAssetsNative } from '@/parsers'; -import isEmpty from 'lodash/isEmpty'; -import isNil from 'lodash/isNil'; -import { RainbowAddressAssets } from './types'; - -const EMPTY_ARRAY: any = []; - -export function selectUserAssetWithUniqueId(uniqueId: string) { - return (accountAssets: RainbowAddressAssets) => { - return accountAssets?.[uniqueId]; - }; -} - -export function selectSortedUserAssets(nativeCurrency: NativeCurrencyKey) { - return (accountAssets: RainbowAddressAssets) => { - return sortAssetsByNativeAmount(accountAssets, nativeCurrency); - }; -} - -const sortAssetsByNativeAmount = (accountAssets: RainbowAddressAssets, nativeCurrency: NativeCurrencyKey): ParsedAddressAsset[] => { - let assetsNativePrices = Object.values(accountAssets); - - if (!isEmpty(assetsNativePrices)) { - assetsNativePrices = parseAssetsNative(assetsNativePrices, nativeCurrency); - } - const { hasValue = EMPTY_ARRAY, noValue = EMPTY_ARRAY } = groupAssetsByMarketValue(assetsNativePrices); - - const sortedAssetsNoShitcoins = hasValue.sort((a: any, b: any) => { - const itemA = Number(a.native?.balance?.amount) ?? 0; - const itemB = Number(b.native?.balance?.amount) ?? 0; - - return itemA < itemB ? 1 : -1; - }); - - const sortedShitcoins = noValue.sort((a: any, b: any) => { - const itemA = a.name; - const itemB = b.name; - - return itemA > itemB ? 1 : -1; - }); - - const sortedAssets = sortedAssetsNoShitcoins.concat(sortedShitcoins); - - return sortedAssets; -}; - -const groupAssetsByMarketValue = (assets: any) => - assets.reduce( - (acc: any, asset: any) => { - if (isNil(asset.native)) { - acc.noValue.push(asset); - } else { - acc.hasValue.push(asset); - } - - return acc; - }, - { - hasValue: [], - noValue: [], - } - ); diff --git a/src/resources/assets/assets.ts b/src/resources/assets/assets.ts index 057963856bb..d64dacd9948 100644 --- a/src/resources/assets/assets.ts +++ b/src/resources/assets/assets.ts @@ -1,8 +1,8 @@ import lang from 'i18n-js'; -import isEmpty from 'lodash/isEmpty'; -import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; import { isNativeAsset } from '@/handlers/assets'; import { convertRawAmountToBalance } from '@/helpers/utilities'; +import isEmpty from 'lodash/isEmpty'; +import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; import { queryClient } from '@/react-query'; import { positionsQueryKey } from '@/resources/defi/PositionsQuery'; import { RainbowPositions } from '@/resources/defi/types'; diff --git a/src/resources/assets/useSortedUserAssets.ts b/src/resources/assets/useSortedUserAssets.ts deleted file mode 100644 index b7e80d92c71..00000000000 --- a/src/resources/assets/useSortedUserAssets.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useAccountSettings } from '@/hooks'; -import { selectSortedUserAssets } from '@/resources/assets/assetSelectors'; -import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; - -export function useSortedUserAssets() { - const { accountAddress, nativeCurrency } = useAccountSettings(); - const { connectedToHardhat } = useConnectedToHardhatStore(); - - return useUserAssets( - { - address: accountAddress, - currency: nativeCurrency, - connectedToHardhat, - }, - { - select: selectSortedUserAssets(nativeCurrency), - } - ); -} diff --git a/src/resources/assets/useUserAsset.ts b/src/resources/assets/useUserAsset.ts deleted file mode 100644 index 4ee1dea9de1..00000000000 --- a/src/resources/assets/useUserAsset.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ChainId } from '@/state/backendNetworks/types'; -import { useAccountSettings } from '@/hooks'; -import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; -import { selectUserAssetWithUniqueId } from '@/resources/assets/assetSelectors'; -import { getUniqueId } from '@/utils/ethereumUtils'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; - -export function useUserAsset(uniqueId: string) { - const { accountAddress, nativeCurrency } = useAccountSettings(); - const { connectedToHardhat } = useConnectedToHardhatStore(); - - return useUserAssets( - { - address: accountAddress, - currency: nativeCurrency, - connectedToHardhat, - }, - { - select: selectUserAssetWithUniqueId(uniqueId), - } - ); -} - -export function useUserNativeNetworkAsset(chainId: ChainId) { - const nativeCurrency = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]; - const { address } = nativeCurrency; - const uniqueId = getUniqueId(address, chainId); - return useUserAsset(uniqueId); -} diff --git a/src/resources/assets/useUserAssetCount.ts b/src/resources/assets/useUserAssetCount.ts deleted file mode 100644 index 7cf00ced409..00000000000 --- a/src/resources/assets/useUserAssetCount.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useAccountSettings } from '@/hooks'; -import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; -import { RainbowAddressAssets } from './types'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; - -const countSelector = (accountAssets: RainbowAddressAssets) => accountAssets?.length; - -export function useUserAssetCount() { - const { accountAddress, nativeCurrency } = useAccountSettings(); - const { connectedToHardhat } = useConnectedToHardhatStore(); - - return useUserAssets( - { - address: accountAddress, - currency: nativeCurrency, - connectedToHardhat, - }, - { - select: countSelector, - } - ); -} diff --git a/src/screens/SendSheet.tsx b/src/screens/SendSheet.tsx index 1369c2ec051..f7f71b7173f 100644 --- a/src/screens/SendSheet.tsx +++ b/src/screens/SendSheet.tsx @@ -46,7 +46,6 @@ import { loadWallet, sendTransaction } from '@/model/wallet'; import { useNavigation } from '@/navigation/Navigation'; import { parseGasParamsForTransaction } from '@/parsers'; import { rainbowTokenList } from '@/references'; -import { useSortedUserAssets } from '@/resources/assets/useSortedUserAssets'; import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; import { borders } from '@/styles'; @@ -63,13 +62,13 @@ import { getNextNonce } from '@/state/nonces'; import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { REGISTRATION_STEPS } from '@/helpers/ens'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; import { ChainId } from '@/state/backendNetworks/types'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { RootStackParamList } from '@/navigation/types'; import { ThemeContextProps, useTheme } from '@/theme'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { Contact } from '@/redux/contacts'; -import { useUserAssetsStore } from '@/state/assets/userAssets'; const sheetHeight = deviceUtils.dimensions.height - (IS_ANDROID ? 30 : 10); const statusBarHeight = IS_IOS ? safeAreaInsetValues.top : StatusBar.currentHeight; @@ -117,7 +116,7 @@ type OnSubmitProps = { export default function SendSheet() { const { goBack, navigate } = useNavigation(); - const { data: sortedAssets } = useSortedUserAssets(); + const sortedAssets = useUserAssetsStore(state => state.legacyUserAssets); const { gasFeeParamsBySpeed, gasLimit, diff --git a/src/screens/claimables/transaction/context/TransactionClaimableContext.tsx b/src/screens/claimables/transaction/context/TransactionClaimableContext.tsx index 63c1ce771be..d25448ea23e 100644 --- a/src/screens/claimables/transaction/context/TransactionClaimableContext.tsx +++ b/src/screens/claimables/transaction/context/TransactionClaimableContext.tsx @@ -15,7 +15,6 @@ import { convertAmountToNativeDisplayWorklet, add, } from '@/helpers/utilities'; -import { useUserNativeNetworkAsset } from '@/resources/assets/useUserAsset'; import { GasSpeed } from '@/__swaps__/types/gas'; import { getGasSettingsBySpeed, useGasSettings } from '@/__swaps__/screens/Swap/hooks/useSelectedGas'; import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; @@ -41,6 +40,7 @@ import { getRemoteConfig } from '@/model/remoteConfig'; import { estimateClaimUnlockSwapGasLimit } from '../estimateGas'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import showWalletErrorAlert from '@/helpers/support'; +import { userAssetsStore } from '@/state/assets/userAssets'; enum ErrorMessages { SWAP_ERROR = 'Failed to swap claimed asset due to swap action error', @@ -141,8 +141,6 @@ export function TransactionClaimableContextProvider({ const gasSettings = useGasSettings(claimable.chainId, GasSpeed.FAST); - const { data: userNativeNetworkAsset, isLoading: isLoadingNativeNetworkAsset } = useUserNativeNetworkAsset(claimable.chainId); - const updateQuoteState = useCallback(async () => { if (!outputConfig?.token || !outputConfig.chainId || !outputTokenAddress) { logger.warn('[TransactionClaimableContext]: Somehow entered unreachable state in updateQuote'); @@ -220,11 +218,7 @@ export function TransactionClaimableContextProvider({ const provider = useMemo(() => getProvider({ chainId: claimable.chainId }), [claimable.chainId]); // make sure we have necessary data before attempting gas estimation - const canEstimateGas = !!( - !isLoadingNativeNetworkAsset && - gasSettings && - (!requiresSwap || (quoteState.quote && quoteState.status === 'success')) - ); + const canEstimateGas = !!(gasSettings && (!requiresSwap || (quoteState.quote && quoteState.status === 'success'))); const updateGasState = useCallback(async () => { try { @@ -252,12 +246,13 @@ export function TransactionClaimableContextProvider({ const gasFeeWei = calculateGasFeeWorklet(gasSettings, gasLimit); const nativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset()[claimable.chainId]; + const userNativeAsset = userAssetsStore.getState().getNativeAssetForChain(claimable.chainId); const gasFeeNativeToken = formatUnits(safeBigInt(gasFeeWei), nativeAsset.decimals); - const userBalance = userNativeNetworkAsset?.balance?.amount || '0'; + const userBalance = userNativeAsset?.balance?.amount || '0'; const sufficientGas = lessThanOrEqualToWorklet(gasFeeNativeToken, userBalance); - const networkAssetPrice = userNativeNetworkAsset?.price?.value?.toString(); + const networkAssetPrice = userNativeAsset?.price?.value?.toString(); let gasFeeNativeCurrencyDisplay; if (!networkAssetPrice) { @@ -290,8 +285,6 @@ export function TransactionClaimableContextProvider({ accountAddress, quoteState.quote, gasSettings, - userNativeNetworkAsset?.balance?.amount, - userNativeNetworkAsset?.price?.value, gasState.status, nativeCurrency, ]); diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 2109601e25d..d667880acdb 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -5,14 +5,80 @@ import reduxStore, { AppState } from '@/redux/store'; import { supportedNativeCurrencies } from '@/references'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; import { useStore } from 'zustand'; +import { ParsedAddressAsset } from '@/entities'; import { swapsStore } from '@/state/swaps/swapsStore'; import { ChainId } from '@/state/backendNetworks/types'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { useSelector } from 'react-redux'; +import { getUniqueId } from '@/utils/ethereumUtils'; + +type UserAssetsStateToPersist = Omit< + Partial, + | 'currentAbortController' + | 'inputSearchQuery' + | 'searchCache' + | 'getBalanceSortedChainList' + | 'getChainsWithBalance' + | 'getFilteredUserAssetIds' + | 'getHighestValueNativeAsset' + | 'getUserAsset' + | 'getUserAssets' + | 'selectUserAssetIds' + | 'selectUserAssets' + | 'setSearchCache' + | 'setSearchQuery' + | 'setUserAssets' +>; const SEARCH_CACHE_MAX_ENTRIES = 50; +const parsedSearchAssetToParsedAddressAsset = (asset: ParsedSearchAsset): ParsedAddressAsset => ({ + address: asset.address, + balance: { + amount: asset.balance.amount, + display: asset.balance.display, + }, + network: useBackendNetworksStore.getState().getChainsName()[asset.chainId], + name: asset.name, + chainId: asset.chainId, + color: asset.colors?.primary ?? asset.colors?.fallback, + colors: asset.colors?.primary + ? { + primary: asset.colors.primary, + fallback: asset.colors.fallback, + shadow: asset.colors.shadow, + } + : undefined, + decimals: asset.decimals, + highLiquidity: asset.highLiquidity, + icon_url: asset.icon_url, + id: asset.networks?.[ChainId.mainnet]?.address, + isNativeAsset: asset.isNativeAsset, + price: { + changed_at: undefined, + relative_change_24h: asset.price?.relative_change_24h, + value: asset.price?.value, + }, + mainnet_address: asset.mainnetAddress, + native: { + balance: { + amount: asset.native.balance.amount, + display: asset.native.balance.display, + }, + change: asset.native.price?.change, + price: { + amount: asset.native.price?.amount?.toString(), + display: asset.native.price?.display, + }, + }, + shadowColor: asset.colors?.shadow, + symbol: asset.symbol, + type: asset.type, + uniqueId: asset.uniqueId, +}); + const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const getSearchQueryKey = ({ filter, searchQuery }: { filter: UserAssetFilter; searchQuery: string }) => `${filter}${searchQuery}`; const getDefaultCacheKeys = (): Set => { @@ -35,41 +101,27 @@ export interface UserAssetsState { inputSearchQuery: string; searchCache: Map; userAssets: Map; + legacyUserAssets: ParsedAddressAsset[]; + isLoadingUserAssets: boolean; getBalanceSortedChainList: () => ChainId[]; getChainsWithBalance: () => ChainId[]; getFilteredUserAssetIds: () => UniqueId[]; getHighestValueNativeAsset: () => ParsedSearchAsset | null; getUserAsset: (uniqueId: UniqueId) => ParsedSearchAsset | null; + getLegacyUserAsset: (uniqueId: UniqueId) => ParsedAddressAsset | null; + getNativeAssetForChain: (chainId: ChainId) => ParsedSearchAsset | null; getUserAssets: () => ParsedSearchAsset[]; selectUserAssetIds: (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) => Generator; selectUserAssets: (selector: (asset: ParsedSearchAsset) => boolean) => Generator<[UniqueId, ParsedSearchAsset], void, unknown>; setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => void; setSearchQuery: (query: string) => void; - setUserAssets: (userAssets: Map | ParsedSearchAsset[]) => void; + setUserAssets: (userAssets: ParsedSearchAsset[]) => void; hiddenAssets: Set; getHiddenAssetsIds: () => UniqueId[]; setHiddenAssets: (uniqueIds: UniqueId[]) => void; } -type UserAssetsStateToPersist = Omit< - Partial, - | 'currentAbortController' - | 'inputSearchQuery' - | 'searchCache' - | 'getBalanceSortedChainList' - | 'getChainsWithBalance' - | 'getFilteredUserAssetIds' - | 'getHighestValueNativeAsset' - | 'getUserAsset' - | 'getUserAssets' - | 'selectUserAssetIds' - | 'selectUserAssets' - | 'setSearchCache' - | 'setSearchQuery' - | 'setUserAssets' ->; - // NOTE: We are serializing Map as an Array<[UniqueId, ParsedSearchAsset]> type UserAssetsStateToPersistWithTransforms = Omit< UserAssetsStateToPersist, @@ -170,6 +222,8 @@ export const createUserAssetsStore = (address: Address | string) => inputSearchQuery: '', searchCache: new Map(), userAssets: new Map(), + legacyUserAssets: [], + isLoadingUserAssets: true, getBalanceSortedChainList: () => { const chainBalances = [...get().chainBalances.entries()]; @@ -223,7 +277,7 @@ export const createUserAssetsStore = (address: Address | string) => const preferredNetwork = swapsStore.getState().preferredNetwork; const assets = get().userAssets; - let highestValueEth = null; + let highestValueNativeAsset = null; for (const [, asset] of assets) { if (!asset.isNativeAsset) continue; @@ -232,16 +286,28 @@ export const createUserAssetsStore = (address: Address | string) => return asset; } - if (!highestValueEth || asset.balance > highestValueEth.balance) { - highestValueEth = asset; + if (!highestValueNativeAsset || asset.balance > highestValueNativeAsset.balance) { + highestValueNativeAsset = asset; } } - return highestValueEth; + return highestValueNativeAsset; + }, + + getNativeAssetForChain: (chainId: ChainId) => { + const nativeAssetAddress = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId].address; + const nativeAssetUniqueId = getUniqueId(nativeAssetAddress, chainId); + return get().userAssets.get(nativeAssetUniqueId) || null; }, getUserAsset: (uniqueId: UniqueId) => get().userAssets.get(uniqueId) || null, + getLegacyUserAsset: (uniqueId: UniqueId) => { + const asset = get().userAssets.get(uniqueId); + if (!asset) return null; + return parsedSearchAssetToParsedAddressAsset(asset); + }, + getUserAssets: () => Array.from(get().userAssets.values()) || [], selectUserAssetIds: function* (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) { @@ -305,7 +371,7 @@ export const createUserAssetsStore = (address: Address | string) => }); }, - setUserAssets: (userAssets: Map | ParsedSearchAsset[]) => + setUserAssets: (userAssets: ParsedSearchAsset[]) => set(() => { const idsByChain = new Map(); const unsortedChainBalances = new Map(); @@ -336,9 +402,9 @@ export const createUserAssetsStore = (address: Address | string) => idsByChain.set(chainId, idsByChain.get(chainId) || []); }); - const isMap = userAssets instanceof Map; - const allIdsArray = isMap ? Array.from(userAssets.keys()) : userAssets.map(asset => asset.uniqueId); - const userAssetsMap = isMap ? userAssets : new Map(userAssets.map(asset => [asset.uniqueId, asset])); + const allIdsArray = userAssets.map(asset => asset.uniqueId); + const userAssetsMap = new Map(userAssets.map(asset => [asset.uniqueId, asset])); + const legacyUserAssets = userAssets.map(asset => parsedSearchAssetToParsedAddressAsset(asset)); idsByChain.set('all', allIdsArray); @@ -358,15 +424,14 @@ export const createUserAssetsStore = (address: Address | string) => searchCache.set('all', filteredAllIdsArray); - if (isMap) { - return { chainBalances, idsByChain, searchCache, userAssets }; - } else - return { - chainBalances, - idsByChain, - searchCache, - userAssets: userAssetsMap, - }; + return { + chainBalances, + idsByChain, + legacyUserAssets, + searchCache, + userAssets: userAssetsMap, + isLoadingUserAssets: false, + }; }), hiddenAssets: new Set(), @@ -383,7 +448,6 @@ export const createUserAssetsStore = (address: Address | string) => hiddenAssets.add(uniqueId); } }); - return { hiddenAssets }; }); }, @@ -395,6 +459,7 @@ export const createUserAssetsStore = (address: Address | string) => filter: state.filter, idsByChain: state.idsByChain, userAssets: state.userAssets, + legacyUserAssets: state.legacyUserAssets, hiddenAssets: state.hiddenAssets, }), version: 1, diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index e8722cddfce..06d94eb832a 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import { useAccountSettings } from '@/hooks'; import { userAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; @@ -5,8 +6,9 @@ import { selectUserAssetsList, selectorFilterByUserChains } from '@/__swaps__/sc import { ParsedSearchAsset } from '@/__swaps__/types/assets'; import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; import { ChainId } from '@/state/backendNetworks/types'; +import { IS_TEST } from '@/env'; -export const UserAssetsSync = function UserAssetsSync() { +function UserAssetsSyncComponent() { const { accountAddress, nativeCurrency: currentCurrency } = useAccountSettings(); const isSwapsOpen = useSwapsStore(state => state.isSwapsOpen); const isUserAssetsStoreMissingData = userAssetsStore.getState().getUserAssets()?.length === 0; @@ -39,4 +41,6 @@ export const UserAssetsSync = function UserAssetsSync() { ); return null; -}; +} + +export const UserAssetsSync = IS_TEST ? UserAssetsSyncComponent : memo(UserAssetsSyncComponent); diff --git a/src/utils/ethereumUtils.ts b/src/utils/ethereumUtils.ts index 7c095eb3676..74dbcb14969 100644 --- a/src/utils/ethereumUtils.ts +++ b/src/utils/ethereumUtils.ts @@ -1,8 +1,6 @@ import { BigNumberish } from '@ethersproject/bignumber'; import { StaticJsonRpcProvider, TransactionRequest } from '@ethersproject/providers'; import { serialize } from '@ethersproject/transactions'; -import { RainbowAddressAssets } from '@/resources/assets/types'; -import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { queryClient } from '@/react-query'; // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'eth-... Remove this comment to see the full error message @@ -42,7 +40,7 @@ import { import { ChainId, Network } from '@/state/backendNetworks/types'; import { AddressOrEth } from '@/__swaps__/types/assets'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { userAssetsStore } from '@/state/assets/userAssets'; /** * @deprecated - use `getUniqueId` instead for chainIds @@ -148,23 +146,6 @@ const getAsset = (accountAssets: Record, uniqueId: E const loweredUniqueId = uniqueId.toLowerCase(); return accountAssets[loweredUniqueId]; }; - -const getUserAssetFromCache = (uniqueId: string) => { - const { accountAddress, nativeCurrency } = store.getState().settings; - const connectedToHardhat = useConnectedToHardhatStore.getState().connectedToHardhat; - - const cache = queryClient.getQueryCache(); - - const cachedAddressAssets = (cache.find( - userAssetsQueryKey({ - address: accountAddress, - currency: nativeCurrency, - connectedToHardhat, - }) - )?.state?.data || {}) as RainbowAddressAssets; - return cachedAddressAssets?.[uniqueId]; -}; - const getExternalAssetFromCache = (uniqueId: string) => { const { nativeCurrency } = store.getState().settings; const { address, chainId } = getAddressAndChainIdFromUniqueId(uniqueId); @@ -186,15 +167,14 @@ const getExternalAssetFromCache = (uniqueId: string) => { const getAssetFromAllAssets = (uniqueId: EthereumAddress | undefined) => { const loweredUniqueId = uniqueId?.toLowerCase() ?? ''; - const accountAsset = getUserAssetFromCache(loweredUniqueId); + const accountAsset = userAssetsStore.getState().getLegacyUserAsset(loweredUniqueId); const externalAsset = getExternalAssetFromCache(loweredUniqueId); return accountAsset ?? externalAsset; }; const getAccountAsset = (uniqueId: EthereumAddress | undefined): ParsedAddressAsset | undefined => { const loweredUniqueId = uniqueId?.toLowerCase() ?? ''; - const accountAsset = getUserAssetFromCache(loweredUniqueId); - return accountAsset; + return userAssetsStore.getState().getLegacyUserAsset(loweredUniqueId) ?? undefined; }; const getAssetPrice = (