diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index bc7e2ee5a5d..6368bc161b9 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -300,24 +300,20 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { } } - queryClient.invalidateQueries([ - // old user assets invalidation (will cause a re-fetch) - { - queryKey: userAssetsQueryKey({ - address: parameters.quote.from, - currency: nativeCurrency, - connectedToHardhat, - }), - }, - // new swaps user assets invalidations - { - queryKey: swapsUserAssetsQueryKey({ - address: parameters.quote.from as Address, - currency: nativeCurrency, - testnetMode: !!connectedToHardhat, - }), - }, - ]); + queryClient.invalidateQueries( + userAssetsQueryKey({ + address: parameters.quote.from, + currency: nativeCurrency, + connectedToHardhat, + }) + ); + queryClient.invalidateQueries( + swapsUserAssetsQueryKey({ + address: parameters.quote.from as Address, + currency: nativeCurrency, + testnetMode: !!connectedToHardhat, + }) + ); swapsStore.getState().addRecentSwap(parameters.assetToBuy as ExtendedAnimatedAssetWithColors); clearCustomGasSettings(chainId); diff --git a/src/components/DappBrowser/control-panel/ControlPanel.tsx b/src/components/DappBrowser/control-panel/ControlPanel.tsx index 60d60904d56..a0d844c4fdc 100644 --- a/src/components/DappBrowser/control-panel/ControlPanel.tsx +++ b/src/components/DappBrowser/control-panel/ControlPanel.tsx @@ -43,9 +43,8 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import { toHex } from 'viem'; import { RainbowNetworks } from '@/networks'; import * as i18n from '@/languages'; -import { convertAmountToNativeDisplay } from '@/helpers/utilities'; -import { useDispatch, useSelector } from 'react-redux'; -import store, { AppState } from '@/redux/store'; +import { useDispatch } from 'react-redux'; +import store from '@/redux/store'; import { getDappHost } from '@/utils/connectedApps'; import WebView from 'react-native-webview'; import { Navigation, useNavigation } from '@/navigation'; @@ -64,6 +63,7 @@ import { getRemoteConfig } from '@/model/remoteConfig'; import { SWAPS_V2, useExperimentalFlag } from '@/config'; import { swapsStore } from '@/state/swaps/swapsStore'; import { userAssetsStore } from '@/state/assets/userAssets'; +import { greaterThan } from '@/helpers/utilities'; const PAGES = { HOME: 'home', @@ -88,7 +88,6 @@ export const ControlPanel = () => { const { params: { activeTabRef }, } = useRoute>(); - const nativeCurrency = useSelector((state: AppState) => state.settings.nativeCurrency); const walletsWithBalancesAndNames = useWalletsWithBalancesAndNames(); const activeTabUrl = useBrowserStore(state => state.getActiveTabUrl()); const activeTabHost = getDappHost(activeTabUrl || '') || DEFAULT_TAB_URL; @@ -140,12 +139,14 @@ export const ControlPanel = () => { const bluetoothWallets: ControlPanelMenuItemProps[] = []; const readOnlyWallets: ControlPanelMenuItemProps[] = []; - const accountBalances: Record = {}; + const accountBalances: Record = {}; Object.values(walletsWithBalancesAndNames).forEach(wallet => { wallet.addresses .filter(account => account.visible) .forEach(account => { + const balanceText = account.balances ? account.balances.totalBalanceDisplay : i18n.t(i18n.l.wallet.change_wallet.loading_balance); + const item: ControlPanelMenuItemProps = { IconComponent: account.image ? ( @@ -153,20 +154,14 @@ export const ControlPanel = () => { ), label: removeFirstEmojiFromString(account.label) || address(account.address, 6, 4), - secondaryLabel: - // eslint-disable-next-line no-nested-ternary - wallet.type === WalletTypes.readOnly - ? i18n.t(i18n.l.wallet.change_wallet.watching) - : account.balance - ? convertAmountToNativeDisplay(account.balance, nativeCurrency) - : i18n.t(i18n.l.wallet.change_wallet.no_balance), + secondaryLabel: wallet.type === WalletTypes.readOnly ? i18n.t(i18n.l.wallet.change_wallet.watching) : balanceText, uniqueId: account.address, color: colors.avatarBackgrounds[account.color], imageUrl: account.image || undefined, selected: account.address === currentAddress, }; - accountBalances[account.address] = Number(account.balance); + accountBalances[account.address] = account.balances?.totalBalanceAmount; if ([WalletTypes.mnemonic, WalletTypes.seed, WalletTypes.privateKey].includes(wallet.type)) { sortedWallets.push(item); @@ -178,13 +173,13 @@ export const ControlPanel = () => { }); }); - sortedWallets.sort((a, b) => accountBalances[b.uniqueId] - accountBalances[a.uniqueId]); - bluetoothWallets.sort((a, b) => accountBalances[b.uniqueId] - accountBalances[a.uniqueId]); + sortedWallets.sort((a, b) => (greaterThan(accountBalances[b.uniqueId], accountBalances[a.uniqueId]) ? 1 : -1)); + bluetoothWallets.sort((a, b) => (greaterThan(accountBalances[b.uniqueId], accountBalances[a.uniqueId]) ? 1 : -1)); const sortedItems = [...sortedWallets, ...bluetoothWallets, ...readOnlyWallets]; return sortedItems; - }, [walletsWithBalancesAndNames, currentAddress, nativeCurrency]); + }, [walletsWithBalancesAndNames, currentAddress]); const { testnetsEnabled } = store.getState().settings; diff --git a/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx b/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx index 853fbe1cd9c..afbe410d580 100644 --- a/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx +++ b/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx @@ -111,7 +111,7 @@ function rowRenderer(type: CellType, { uid }: { uid: string }, _: unknown, exten ); diff --git a/src/components/asset-list/RecyclerAssetList2/core/ViewTypes.ts b/src/components/asset-list/RecyclerAssetList2/core/ViewTypes.ts index 697d5aca3ca..ab6df6115e6 100644 --- a/src/components/asset-list/RecyclerAssetList2/core/ViewTypes.ts +++ b/src/components/asset-list/RecyclerAssetList2/core/ViewTypes.ts @@ -54,9 +54,9 @@ export type AssetListHeaderExtraData = { }; export type AssetsHeaderExtraData = { - type: CellType.PROFILE_STICKY_HEADER; + type: CellType.PROFILE_BALANCE_ROW; value: string; - isLoadingUserAssets: boolean; + isLoadingBalance: boolean; }; export type CoinExtraData = { type: CellType.COIN; uniqueId: string }; export type NFTExtraData = { diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileBalanceRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileBalanceRow.tsx index 9e219feb0fd..e84cc3d5f72 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileBalanceRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileBalanceRow.tsx @@ -4,13 +4,13 @@ import { Box, Heading } from '@/design-system'; export const ProfileBalanceRowHeight = 24; -export function ProfileBalanceRow({ totalValue, isLoadingUserAssets }: { totalValue: string; isLoadingUserAssets: boolean }) { +export function ProfileBalanceRow({ totalValue, isLoadingBalance }: { totalValue: string; isLoadingBalance: boolean }) { const placeholderHeight = ProfileBalanceRowHeight; const placeholderWidth = 200; return ( <> - {isLoadingUserAssets ? ( + {isLoadingBalance ? ( diff --git a/src/components/backup/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index 71a778b2252..92d62c01de2 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -1,6 +1,6 @@ /* eslint-disable no-promise-executor-return */ -import { useCallback, useState } from 'react'; -import { backupAllWalletsToCloud, getLocalBackupPassword, saveLocalBackupPassword } from '@/model/backup'; +import { useCallback, useMemo, useState } from 'react'; +import { backupAllWalletsToCloud, findLatestBackUp, getLocalBackupPassword, saveLocalBackupPassword } from '@/model/backup'; import { useCloudBackups } from './CloudBackupProvider'; import { cloudPlatform } from '@/utils/platform'; import { analytics } from '@/analytics'; @@ -34,7 +34,8 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr const { fetchBackups } = useCloudBackups(); const walletCloudBackup = useWalletCloudBackup(); - const { latestBackup, wallets } = useWallets(); + const { wallets } = useWallets(); + const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]); const [loading, setLoading] = useState('none'); const [password, setPassword] = useState(''); diff --git a/src/components/change-wallet/AddressRow.tsx b/src/components/change-wallet/AddressRow.tsx index 3b638cbf248..a67479f22a6 100644 --- a/src/components/change-wallet/AddressRow.tsx +++ b/src/components/change-wallet/AddressRow.tsx @@ -22,9 +22,6 @@ import { EditWalletContextMenuActions } from '@/screens/ChangeWalletSheet'; import { toChecksumAddress } from '@/handlers/web3'; import { IS_IOS, IS_ANDROID } from '@/env'; import { ContextMenu } from '../context-menu'; -import { convertAmountToNativeDisplay } from '@/helpers/utilities'; -import { useSelector } from 'react-redux'; -import { AppState } from '@/redux/store'; import { useForegroundColor } from '@/design-system'; const maxAccountLabelWidth = deviceUtils.dimensions.width - 88; @@ -121,22 +118,15 @@ interface AddressRowProps { } export default function AddressRow({ contextMenuActions, data, editMode, onPress }: AddressRowProps) { - const nativeCurrency = useSelector((state: AppState) => state.settings.nativeCurrency); const notificationsEnabled = useExperimentalFlag(NOTIFICATIONS); - const { address, balance, color: accountColor, ens, image: accountImage, isSelected, isReadOnly, isLedger, label, walletId } = data; + const { address, balances, color: accountColor, ens, image: accountImage, isSelected, isReadOnly, isLedger, label, walletId } = data; const { colors, isDarkMode } = useTheme(); const labelQuaternary = useForegroundColor('labelQuaternary'); - const cleanedUpBalance = useMemo(() => { - if (balance) { - return convertAmountToNativeDisplay(balance, nativeCurrency); - } else { - return lang.t('wallet.change_wallet.no_balance'); - } - }, [balance, nativeCurrency]); + const balanceText = balances ? balances.totalBalanceDisplay : lang.t('wallet.change_wallet.loading_balance'); const cleanedUpLabel = useMemo(() => removeFirstEmojiFromString(label), [label]); @@ -254,7 +244,7 @@ export default function AddressRow({ contextMenuActions, data, editMode, onPress {walletName} - {cleanedUpBalance} + {balanceText} diff --git a/src/components/contacts/ContactRow.js b/src/components/contacts/ContactRow.js index f1a22d53236..b213d33dd23 100644 --- a/src/components/contacts/ContactRow.js +++ b/src/components/contacts/ContactRow.js @@ -11,12 +11,11 @@ import useExperimentalFlag, { PROFILES } from '@/config/experimentalHooks'; import { fetchReverseRecord } from '@/handlers/ens'; import { ENS_DOMAIN } from '@/helpers/ens'; import { isENSAddressFormat, isValidDomainFormat } from '@/helpers/validators'; -import { useAccountSettings, useContacts, useDimensions, useENSAvatar } from '@/hooks'; +import { useContacts, useDimensions, useENSAvatar } from '@/hooks'; import styled from '@/styled-thing'; import { margin } from '@/styles'; import { addressHashedColorIndex, addressHashedEmoji } from '@/utils/profileUtils'; import * as i18n from '@/languages'; -import { convertAmountToNativeDisplay } from '@/helpers/utilities'; const ContactAddress = styled(TruncatedAddress).attrs(({ theme: { colors }, lite }) => ({ align: 'left', @@ -58,17 +57,10 @@ const ContactRow = ({ address, color, nickname, symmetricalMargins, ...props }, const profilesEnabled = useExperimentalFlag(PROFILES); const { width: deviceWidth } = useDimensions(); const { onAddOrUpdateContacts } = useContacts(); - const { nativeCurrency } = useAccountSettings(); const { colors } = useTheme(); - const { accountType, balance, ens, image, label, network, onPress, showcaseItem, testID } = props; + const { accountType, balances, ens, image, label, network, onPress, showcaseItem, testID } = props; - const cleanedUpBalance = useMemo(() => { - if (balance) { - return convertAmountToNativeDisplay(balance, nativeCurrency); - } else { - return i18n.t(i18n.l.wallet.change_wallet.no_balance); - } - }, [balance, nativeCurrency]); + const balanceText = balances ? balances.totalBalanceDisplay : i18n.t(i18n.l.wallet.change_wallet.loading_balance); // show avatar for contact rows that are accounts, not contacts const avatar = accountType !== 'contacts' ? returnStringFirstEmoji(label) || profileUtils.addressHashedEmoji(address) : null; @@ -141,7 +133,7 @@ const ContactRow = ({ address, color, nickname, symmetricalMargins, ...props }, )} - {cleanedUpBalance} + {balanceText} ) : ( diff --git a/src/components/ens-profile/ActionButtons/MoreButton.tsx b/src/components/ens-profile/ActionButtons/MoreButton.tsx index 95b96316997..c9ac185cef6 100644 --- a/src/components/ens-profile/ActionButtons/MoreButton.tsx +++ b/src/components/ens-profile/ActionButtons/MoreButton.tsx @@ -6,7 +6,7 @@ import { MenuActionConfig } from 'react-native-ios-context-menu'; import { showDeleteContactActionSheet } from '../../contacts'; import More from '../MoreButton/MoreButton'; import ContextMenuButton from '@/components/native-context-menu/contextMenu'; -import { useClipboard, useContacts, useWallets, useWatchWallet } from '@/hooks'; +import { useClipboard, useContacts, useSwitchWallet, useWallets, useWatchWallet } from '@/hooks'; import { useNavigation } from '@/navigation'; import { RAINBOW_PROFILES_BASE_URL } from '@/references'; import Routes from '@/navigation/routesNames'; @@ -24,7 +24,8 @@ const ACTIONS = { }; export default function MoreButton({ address, ensName }: { address?: string; ensName?: string }) { - const { switchToWalletWithAddress, selectedWallet } = useWallets(); + const { selectedWallet } = useWallets(); + const { switchToWalletWithAddress } = useSwitchWallet(); const { isWatching } = useWatchWallet({ address }); const { navigate } = useNavigation(); const { setClipboard } = useClipboard(); diff --git a/src/helpers/buildWalletSections.tsx b/src/helpers/buildWalletSections.tsx index 2557bf80487..255add920b8 100644 --- a/src/helpers/buildWalletSections.tsx +++ b/src/helpers/buildWalletSections.tsx @@ -1,6 +1,5 @@ import { createSelector } from 'reselect'; import { buildBriefCoinsList, buildBriefUniqueTokenList } from './assets'; -import { add, convertAmountToNativeDisplay } from './utilities'; import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; import { queryClient } from '@/react-query'; import { positionsQueryKey } from '@/resources/defi/PositionsQuery'; @@ -39,9 +38,11 @@ const EMPTY_WALLET_CONTENT = [ const ONLY_NFTS_CONTENT = [{ type: 'ETH_CARD', uid: 'eth-card' }]; const sortedAssetsSelector = (state: any) => state.sortedAssets; +const accountBalanceDisplaySelector = (state: any) => state.accountBalanceDisplay; const hiddenCoinsSelector = (state: any) => state.hiddenCoins; const isCoinListEditedSelector = (state: any) => state.isCoinListEdited; const isLoadingUserAssetsSelector = (state: any) => state.isLoadingUserAssets; +const isLoadingBalanceSelector = (state: any) => state.isLoadingBalance; const isReadOnlyWalletSelector = (state: any) => state.isReadOnlyWallet; const nativeCurrencySelector = (state: any) => state.nativeCurrency; const pinnedCoinsSelector = (state: any) => state.pinnedCoins; @@ -105,23 +106,15 @@ const withPositionsSection = (isLoadingUserAssets: boolean) => { const withBriefBalanceSection = ( sortedAssets: ParsedAddressAsset[], isLoadingUserAssets: boolean, + isLoadingBalance: boolean, + accountBalanceDisplay: string | undefined, nativeCurrency: NativeCurrencyKey, isCoinListEdited: boolean, pinnedCoins: any, hiddenCoins: any, - collectibles: any, - nftSort: string + collectibles: any ) => { - const { briefAssets, totalBalancesValue } = buildBriefCoinsList(sortedAssets, nativeCurrency, isCoinListEdited, pinnedCoins, hiddenCoins); - - const { accountAddress: address } = store.getState().settings; - const positionsObj: RainbowPositions | undefined = queryClient.getQueryData(positionsQueryKey({ address, currency: nativeCurrency })); - - const positionsTotal = positionsObj?.totals?.total?.amount || '0'; - - const totalBalanceWithPositionsValue = add(totalBalancesValue, positionsTotal); - - const totalValue = convertAmountToNativeDisplay(totalBalanceWithPositionsValue, nativeCurrency); + const { briefAssets } = buildBriefCoinsList(sortedAssets, nativeCurrency, isCoinListEdited, pinnedCoins, hiddenCoins); const hasTokens = briefAssets?.length; const hasNFTs = collectibles?.length; @@ -133,8 +126,6 @@ const withBriefBalanceSection = ( { type: 'PROFILE_STICKY_HEADER', uid: 'assets-profile-header-compact', - value: totalValue, - isLoadingUserAssets, }, { type: 'PROFILE_AVATAR_ROW_SPACE_BEFORE', @@ -156,13 +147,14 @@ const withBriefBalanceSection = ( type: 'PROFILE_NAME_ROW_SPACE_AFTER', uid: 'profile-name-space-after', }, - ...(!hasTokens && !isLoadingUserAssets + ...(!hasTokens && !isLoadingUserAssets && !isLoadingBalance ? [] : [ { type: 'PROFILE_BALANCE_ROW', uid: 'profile-balance', - value: totalValue, + value: accountBalanceDisplay, + isLoadingBalance, }, { type: 'PROFILE_BALANCE_ROW_SPACE_AFTER', @@ -172,13 +164,13 @@ const withBriefBalanceSection = ( { type: 'PROFILE_ACTION_BUTTONS_ROW', uid: 'profile-action-buttons', - value: totalValue, + value: accountBalanceDisplay, }, hasTokens ? { type: 'PROFILE_ACTION_BUTTONS_ROW_SPACE_AFTER', uid: 'profile-action-buttons-space-after', - value: totalValue, + value: accountBalanceDisplay, } : { type: 'BIG_EMPTY_WALLET_SPACER', uid: 'big-empty-wallet-spacer-1' }, ]; @@ -226,6 +218,8 @@ const briefBalanceSectionSelector = createSelector( [ sortedAssetsSelector, isLoadingUserAssetsSelector, + isLoadingBalanceSelector, + accountBalanceDisplaySelector, nativeCurrencySelector, isCoinListEditedSelector, pinnedCoinsSelector, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 048802245d6..f550c40c845 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -94,6 +94,7 @@ export { default as useSwapSettings } from './useSwapSettings'; export { default as useSwapDerivedOutputs } from './useSwapDerivedOutputs'; export { default as useSwapDerivedValues } from './useSwapDerivedValues'; export { default as useSwapRefuel } from './useSwapRefuel'; +export { default as useSwitchWallet } from './useSwitchWallet'; export { default as useTimeout } from './useTimeout'; export { default as useTransactionConfirmation } from './useTransactionConfirmation'; export { default as usePendingTransactions } from './usePendingTransactions'; diff --git a/src/hooks/useRefreshAccountData.ts b/src/hooks/useRefreshAccountData.ts index 4de563b7108..df30a1a18e6 100644 --- a/src/hooks/useRefreshAccountData.ts +++ b/src/hooks/useRefreshAccountData.ts @@ -1,6 +1,6 @@ import { captureException } from '@sentry/react-native'; import delay from 'delay'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { getIsHardhatConnected } from '@/handlers/web3'; import { walletConnectLoadState } from '../redux/walletconnect'; @@ -10,8 +10,12 @@ import { PROFILES, useExperimentalFlag } from '@/config'; import logger from '@/utils/logger'; import { queryClient } from '@/react-query'; import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; +import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { nftsQueryKey } from '@/resources/nfts'; import { positionsQueryKey } from '@/resources/defi/PositionsQuery'; +import { Address } from 'viem'; +import { addysSummaryQueryKey } from '@/resources/summary/summary'; +import useWallets from './useWallets'; export default function useRefreshAccountData() { const dispatch = useDispatch(); @@ -19,25 +23,23 @@ export default function useRefreshAccountData() { const [isRefreshing, setIsRefreshing] = useState(false); const profilesEnabled = useExperimentalFlag(PROFILES); + const { wallets } = useWallets(); + + const allAddresses = useMemo( + () => Object.values(wallets || {}).flatMap(wallet => wallet.addresses.map(account => account.address as Address)), + [wallets] + ); + const fetchAccountData = useCallback(async () => { const connectedToHardhat = getIsHardhatConnected(); - queryClient.invalidateQueries({ - queryKey: nftsQueryKey({ address: accountAddress }), - }); - queryClient.invalidateQueries({ - queryKey: positionsQueryKey({ - address: accountAddress, - currency: nativeCurrency, - }), - }); - queryClient.invalidateQueries({ - queryKey: userAssetsQueryKey({ - address: accountAddress, - currency: nativeCurrency, - connectedToHardhat, - }), - }); + queryClient.invalidateQueries(nftsQueryKey({ address: accountAddress })); + queryClient.invalidateQueries(positionsQueryKey({ address: accountAddress as Address, 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 }) + ); try { const getWalletNames = dispatch(fetchWalletNames()); diff --git a/src/hooks/useSwitchWallet.ts b/src/hooks/useSwitchWallet.ts new file mode 100644 index 00000000000..dd0bda69a0f --- /dev/null +++ b/src/hooks/useSwitchWallet.ts @@ -0,0 +1,30 @@ +import { useDispatch } from 'react-redux'; +import { addressSetSelected, walletsSetSelected } from '../redux/wallets'; +import useInitializeWallet from './useInitializeWallet'; +import { toChecksumAddress } from '@/handlers/web3'; +import { RainbowAccount } from '@/model/wallet'; +import useWallets from './useWallets'; + +export default function useSwitchWallet() { + const initializeWallet = useInitializeWallet(); + const dispatch = useDispatch(); + const { wallets } = useWallets(); + + const switchToWalletWithAddress = async (address: string): Promise => { + const walletKey = Object.keys(wallets!).find(key => { + // Addresses + return wallets![key].addresses.find((account: RainbowAccount) => account.address.toLowerCase() === address.toLowerCase()); + }); + + if (!walletKey) return null; + const p1 = dispatch(walletsSetSelected(wallets![walletKey])); + const p2 = dispatch(addressSetSelected(toChecksumAddress(address)!)); + await Promise.all([p1, p2]); + // @ts-expect-error ts-migrate(2554) FIXME: Expected 8-9 arguments, but got 7. + return initializeWallet(null, null, null, false, false, null, true); + }; + + return { + switchToWalletWithAddress, + }; +} diff --git a/src/hooks/useWalletBalances.ts b/src/hooks/useWalletBalances.ts index bd368aa3042..c4c4e818d76 100644 --- a/src/hooks/useWalletBalances.ts +++ b/src/hooks/useWalletBalances.ts @@ -3,19 +3,86 @@ import { useMemo } from 'react'; import { Address } from 'viem'; import useAccountSettings from './useAccountSettings'; import { useAddysSummary } from '@/resources/summary/summary'; +import { useQueries } from '@tanstack/react-query'; +import { fetchPositions, positionsQueryKey } from '@/resources/defi/PositionsQuery'; +import { RainbowPositions } from '@/resources/defi/types'; +import { add, convertAmountToNativeDisplay } from '@/helpers/utilities'; +import { queryClient } from '@/react-query'; -const useWalletBalances = (wallets: AllRainbowWallets) => { +const QUERY_CONFIG = { + staleTime: 1000 * 60 * 2, // 2 minutes + cacheTime: 1000 * 60 * 60 * 24, // 24 hours +}; + +type WalletBalance = { + assetBalanceAmount: string; + assetBalanceDisplay: string; + positionsBalanceAmount: string; + positionsBalanceDisplay: string; + totalBalanceAmount: string; + totalBalanceDisplay: string; +}; + +type WalletBalanceResult = { + balances: Record; + isLoading: boolean; +}; + +const useWalletBalances = (wallets: AllRainbowWallets): WalletBalanceResult => { const { nativeCurrency } = useAccountSettings(); - const walletAddresses: Address[] = useMemo( + const allAddresses = useMemo( () => Object.values(wallets).flatMap(wallet => wallet.addresses.map(account => account.address as Address)), [wallets] ); - const { data, isLoading } = useAddysSummary({ addresses: walletAddresses, currency: nativeCurrency }); + const { data: summaryData, isLoading: isSummaryLoading } = useAddysSummary( + { + addresses: allAddresses, + currency: nativeCurrency, + }, + QUERY_CONFIG + ); + + const positionQueries = useQueries({ + queries: allAddresses.map(address => ({ + queryKey: positionsQueryKey({ address, currency: nativeCurrency }), + queryFn: () => fetchPositions({ address, currency: nativeCurrency }), + enabled: !!address, + ...QUERY_CONFIG, + })), + }); + + const isLoading = isSummaryLoading || positionQueries.some(query => query.isLoading); + + const balances = useMemo(() => { + const result: Record = {}; + + if (isLoading) return result; + + for (const address of allAddresses) { + const lowerCaseAddress = address.toLowerCase() as Address; + const assetBalance = summaryData?.data?.addresses?.[lowerCaseAddress]?.summary?.asset_value.toString() || '0'; + + const positionData = queryClient.getQueryData(positionsQueryKey({ address, currency: nativeCurrency })); + const positionsBalance = positionData?.totals?.total?.amount || '0'; + const totalAccountBalance = add(assetBalance, positionsBalance); + + result[lowerCaseAddress] = { + assetBalanceAmount: assetBalance, + assetBalanceDisplay: convertAmountToNativeDisplay(assetBalance, nativeCurrency), + positionsBalanceAmount: positionsBalance, + positionsBalanceDisplay: convertAmountToNativeDisplay(positionsBalance, nativeCurrency), + totalBalanceAmount: totalAccountBalance, + totalBalanceDisplay: convertAmountToNativeDisplay(totalAccountBalance, nativeCurrency), + }; + } + + return result; + }, [allAddresses, summaryData, nativeCurrency]); return { - balances: data?.data?.addresses, + balances, isLoading, }; }; diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index c0c76e6e0cb..f0efed48182 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -1,10 +1,10 @@ import { captureException } from '@sentry/react-native'; import lang from 'i18n-js'; import { values } from 'lodash'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { Linking } from 'react-native'; import { useDispatch } from 'react-redux'; -import { addWalletToCloudBackup, backupWalletToCloud } from '../model/backup'; +import { addWalletToCloudBackup, backupWalletToCloud, findLatestBackUp } from '../model/backup'; import { setWalletBackedUp } from '../redux/wallets'; import { cloudPlatform } from '../utils/platform'; import useWallets from './useWallets'; @@ -40,7 +40,8 @@ export function getUserError(e: Error) { export default function useWalletCloudBackup() { const dispatch = useDispatch(); - const { latestBackup, wallets } = useWallets(); + const { wallets } = useWallets(); + const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]); const walletCloudBackup = useCallback( async ({ diff --git a/src/hooks/useWalletSectionsData.ts b/src/hooks/useWalletSectionsData.ts index 9b2b60c9c4c..7fe57995040 100644 --- a/src/hooks/useWalletSectionsData.ts +++ b/src/hooks/useWalletSectionsData.ts @@ -11,12 +11,14 @@ import { buildBriefWalletSectionsSelector } from '@/helpers/buildWalletSections' import { useSortedUserAssets } from '@/resources/assets/useSortedUserAssets'; import { useLegacyNFTs } from '@/resources/nfts'; import useNftSort from './useNFTsSortBy'; +import useWalletsWithBalancesAndNames from './useWalletsWithBalancesAndNames'; export default function useWalletSectionsData({ type, }: { type?: string; } = {}) { + const { selectedWallet, isReadOnlyWallet } = useWallets(); const { isLoading: isLoadingUserAssets, data: sortedAssets = [] } = useSortedUserAssets(); const isWalletEthZero = useIsWalletEthZero(); @@ -27,9 +29,15 @@ export default function useWalletSectionsData({ } = useLegacyNFTs({ address: accountAddress, }); + + const walletsWithBalancesAndNames = useWalletsWithBalancesAndNames(); + + const accountWithBalance = walletsWithBalancesAndNames[selectedWallet.id]?.addresses.find( + address => address.address.toLowerCase() === accountAddress.toLowerCase() + ); + const { showcaseTokens } = useShowcaseTokens(); const { hiddenTokens } = useHiddenTokens(); - const { isReadOnlyWallet } = useWallets(); const { hiddenCoinsObj: hiddenCoins, pinnedCoinsObj: pinnedCoins } = useCoinListEditOptions(); @@ -48,6 +56,8 @@ export default function useWalletSectionsData({ pinnedCoins, sendableUniqueTokens, sortedAssets, + accountBalanceDisplay: accountWithBalance?.balances?.totalBalanceDisplay, + isLoadingBalance: !accountWithBalance?.balances, // @ts-expect-error ts-migrate(2698) FIXME: Spread types may only be created from object types... Remove this comment to see the full error message ...isWalletEthZero, hiddenTokens, @@ -64,6 +74,7 @@ export default function useWalletSectionsData({ return { hasNFTs, isEmpty, + isLoadingBalance: !accountWithBalance?.balances, isLoadingUserAssets, isWalletEthZero, briefSectionsData, @@ -78,6 +89,7 @@ export default function useWalletSectionsData({ pinnedCoins, sendableUniqueTokens, sortedAssets, + accountWithBalance, isWalletEthZero, hiddenTokens, isReadOnlyWallet, diff --git a/src/hooks/useWallets.ts b/src/hooks/useWallets.ts index 64d5c2abcf3..38363886917 100644 --- a/src/hooks/useWallets.ts +++ b/src/hooks/useWallets.ts @@ -1,14 +1,8 @@ -import { useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; -import { findLatestBackUp } from '../model/backup'; -import { addressSetSelected, walletsSetSelected } from '../redux/wallets'; -import useInitializeWallet from './useInitializeWallet'; -import { toChecksumAddress } from '@/handlers/web3'; import WalletTypes from '@/helpers/walletTypes'; -import { RainbowAccount, RainbowWallet } from '@/model/wallet'; +import { RainbowWallet } from '@/model/wallet'; import { AppState } from '@/redux/store'; -import logger from '@/utils/logger'; const walletSelector = createSelector( ({ wallets: { isWalletLoading, selected = {} as RainbowWallet, walletNames, wallets } }: AppState) => ({ @@ -19,7 +13,6 @@ const walletSelector = createSelector( }), ({ isWalletLoading, selectedWallet, walletNames, wallets }) => ({ isWalletLoading, - latestBackup: findLatestBackUp(wallets), selectedWallet, walletNames, wallets, @@ -27,42 +20,14 @@ const walletSelector = createSelector( ); export default function useWallets() { - const initializeWallet = useInitializeWallet(); - const dispatch = useDispatch(); - const { isWalletLoading, latestBackup, selectedWallet, walletNames, wallets } = useSelector(walletSelector); - - const isDamaged = useMemo(() => { - const bool = selectedWallet?.damaged; - if (bool) { - logger.sentry('Wallet is damaged. Check values below:'); - logger.sentry('selectedWallet: ', selectedWallet); - logger.sentry('wallets: ', wallets); - } - return bool; - }, [selectedWallet, wallets]); - - const switchToWalletWithAddress = async (address: string): Promise => { - const walletKey = Object.keys(wallets!).find(key => { - // Addresses - return wallets![key].addresses.find((account: RainbowAccount) => account.address.toLowerCase() === address.toLowerCase()); - }); - - if (!walletKey) return null; - const p1 = dispatch(walletsSetSelected(wallets![walletKey])); - const p2 = dispatch(addressSetSelected(toChecksumAddress(address)!)); - await Promise.all([p1, p2]); - // @ts-expect-error ts-migrate(2554) FIXME: Expected 8-9 arguments, but got 7. - return initializeWallet(null, null, null, false, false, null, true); - }; + const { isWalletLoading, selectedWallet, walletNames, wallets } = useSelector(walletSelector); return { - isDamaged, + isDamaged: selectedWallet?.damaged, isReadOnlyWallet: selectedWallet.type === WalletTypes.readOnly, isHardwareWallet: !!selectedWallet.deviceId, isWalletLoading, - latestBackup, selectedWallet, - switchToWalletWithAddress, walletNames, wallets, }; diff --git a/src/hooks/useWalletsWithBalancesAndNames.ts b/src/hooks/useWalletsWithBalancesAndNames.ts index 19fd3eaeab2..dcf48245d98 100644 --- a/src/hooks/useWalletsWithBalancesAndNames.ts +++ b/src/hooks/useWalletsWithBalancesAndNames.ts @@ -6,19 +6,19 @@ import { Address } from 'viem'; export default function useWalletsWithBalancesAndNames() { const { walletNames, wallets } = useWallets(); - const walletBalances = useWalletBalances(wallets!); + const { balances } = useWalletBalances(wallets || {}); const walletsWithBalancesAndNames = useMemo( () => mapValues(wallets, wallet => { const updatedAccounts = (wallet.addresses ?? []).map(account => ({ ...account, - balance: walletBalances?.balances?.[account.address?.toLowerCase() as Address]?.summary?.asset_value, + balances: balances[account.address.toLowerCase() as Address], ens: walletNames[account.address], })); return { ...wallet, addresses: updatedAccounts }; }), - [walletBalances, walletNames, wallets] + [balances, walletNames, wallets] ); return walletsWithBalancesAndNames; diff --git a/src/hooks/useWatchPendingTxs.ts b/src/hooks/useWatchPendingTxs.ts index 777a45aac9b..aed5c6fad25 100644 --- a/src/hooks/useWatchPendingTxs.ts +++ b/src/hooks/useWatchPendingTxs.ts @@ -1,21 +1,22 @@ import { useCallback, useMemo } from 'react'; import useAccountSettings from './useAccountSettings'; import { RainbowTransaction, MinedTransaction } from '@/entities/transactions/transaction'; -import { fetchUserAssets } from '@/resources/assets/UserAssetsQuery'; +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 { Network } from '@/networks/types'; -import { getProviderForNetwork } from '@/handlers/web3'; +import { getIsHardhatConnected, getProviderForNetwork } from '@/handlers/web3'; import { consolidatedTransactionsQueryKey } from '@/resources/transactions/consolidatedTransactions'; import { RainbowNetworks } from '@/networks'; import { queryClient } from '@/react-query/queryClient'; import { getTransactionFlashbotStatus } from '@/handlers/transactions'; import { usePendingTransactionsStore } from '@/state/pendingTransactions'; import { useNonceStore } from '@/state/nonces'; +import { Address } from 'viem'; +import { nftsQueryKey } from '@/resources/nfts'; export const useWatchPendingTransactions = ({ address }: { address: string }) => { - //const { swapRefreshAssets } = useSwapRefreshAssets(); - const { storePendingTransactions, setPendingTransactions } = usePendingTransactionsStore(state => ({ storePendingTransactions: state.pendingTransactions, setPendingTransactions: state.setPendingTransactions, @@ -25,23 +26,29 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => const pendingTransactions = useMemo(() => storePendingTransactions[address] || [], [address, storePendingTransactions]); - const { nativeCurrency, accountAddress } = useAccountSettings(); + const { nativeCurrency } = useAccountSettings(); const refreshAssets = useCallback( - (tx: RainbowTransaction) => { - if (tx.type === 'swap') { - // update swap assets - //swapRefreshAssets(tx.nonce); - } else { - // fetch assets again - fetchUserAssets({ - address: accountAddress, + (_: RainbowTransaction) => { + // NOTE: We have two user assets stores right now, so let's invalidate both queries and trigger a refetch + const connectedToHardhat = getIsHardhatConnected(); + queryClient.invalidateQueries( + userAssetsQueryKey({ + address, currency: nativeCurrency, - connectedToHardhat: false, - }); - } + connectedToHardhat, + }) + ); + queryClient.invalidateQueries( + swapsUserAssetsQueryKey({ + address: address as Address, + currency: nativeCurrency, + testnetMode: !!connectedToHardhat, + }) + ); + queryClient.invalidateQueries(nftsQueryKey({ address })); }, - [accountAddress, nativeCurrency] + [address, nativeCurrency] ); const processFlashbotsTransaction = useCallback(async (tx: RainbowTransaction): Promise => { @@ -183,7 +190,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => const chainIds = RainbowNetworks.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); await queryClient.refetchQueries({ queryKey: consolidatedTransactionsQueryKey({ - address: accountAddress, + address, currency: nativeCurrency, chainIds, }), @@ -193,7 +200,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => setTimeout(() => { queryClient.refetchQueries({ queryKey: consolidatedTransactionsQueryKey({ - address: accountAddress, + address, currency: nativeCurrency, chainIds, }), @@ -201,10 +208,10 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => }, 2000); } setPendingTransactions({ - address: accountAddress, + address, pendingTransactions: newPendingTransactions, }); - }, [accountAddress, nativeCurrency, pendingTransactions, processNonces, processPendingTransaction, setPendingTransactions]); + }, [address, nativeCurrency, pendingTransactions, processNonces, processPendingTransaction, setPendingTransactions]); return { watchPendingTransactions }; }; diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 70c93854a3f..c243830f84d 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2488,7 +2488,7 @@ "balance_title": "Balance", "buy": "Buy", "change_wallet": { - "no_balance": "No Balance", + "loading_balance": "Loading Balance...", "balance_eth": "%{balanceEth} ETH", "watching": "Watching", "ledger": "Ledger" diff --git a/src/notifications/NotificationsHandler.tsx b/src/notifications/NotificationsHandler.tsx index a076c628f09..d809e788d47 100644 --- a/src/notifications/NotificationsHandler.tsx +++ b/src/notifications/NotificationsHandler.tsx @@ -1,5 +1,5 @@ import { PropsWithChildren, useCallback, useEffect, useRef } from 'react'; -import { usePrevious, useWallets } from '@/hooks'; +import { usePrevious, useSwitchWallet, useWallets } from '@/hooks'; import { setupAndroidChannels } from '@/notifications/setupAndroidChannels'; import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; import { @@ -44,9 +44,10 @@ type Callback = () => void; type Props = PropsWithChildren<{ walletReady: boolean }>; export const NotificationsHandler = ({ walletReady }: Props) => { - const wallets = useWallets(); + const { wallets } = useWallets(); + const walletSwitcher = useSwitchWallet(); const dispatch: ThunkDispatch = useDispatch(); - const walletsRef = useRef(wallets); + const walletSwitcherRef = useRef(walletSwitcher); const prevWalletReady = usePrevious(walletReady); const subscriptionChangesListener = useRef(); const onTokenRefreshListener = useRef(); @@ -61,7 +62,7 @@ export const NotificationsHandler = ({ walletReady }: Props) => { We need to save wallets property to a ref in order to have an up-to-date value inside the event listener callbacks closure */ - walletsRef.current = wallets; + walletSwitcherRef.current = walletSwitcher; const onForegroundRemoteNotification = (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => { const type = remoteMessage?.data?.type; @@ -152,12 +153,12 @@ export const NotificationsHandler = ({ walletReady }: Props) => { // casting data payload to type that was agreed on with backend const data = notification.data as unknown as TransactionNotificationData; - const wallets = walletsRef.current; + const walletSwitcher = walletSwitcherRef.current; const { accountAddress, nativeCurrency } = store.getState().settings; let walletAddress: string | null | undefined = accountAddress; if (!isLowerCaseMatch(accountAddress, data.address)) { - walletAddress = await wallets.switchToWalletWithAddress(data.address); + walletAddress = await walletSwitcher.switchToWalletWithAddress(data.address); } if (!walletAddress) { return; @@ -242,7 +243,7 @@ export const NotificationsHandler = ({ walletReady }: Props) => { if (walletReady && !alreadyRanInitialization.current) { const addresses: AddressWithRelationship[] = []; - Object.values(wallets.wallets ?? {}).forEach(wallet => + Object.values(wallets ?? {}).forEach(wallet => wallet?.addresses.forEach( ({ address, visible }: { address: string; visible: boolean }) => visible && diff --git a/src/resources/summary/summary.ts b/src/resources/summary/summary.ts index 04337c43eb2..90705878f46 100644 --- a/src/resources/summary/summary.ts +++ b/src/resources/summary/summary.ts @@ -56,7 +56,7 @@ export type AddysSummaryArgs = { // /////////////////////////////////////////////// // Query Key -const addysSummaryQueryKey = ({ addresses, currency }: AddysSummaryArgs) => +export const addysSummaryQueryKey = ({ addresses, currency }: AddysSummaryArgs) => createQueryKey('addysSummary', { addresses, currency }, { persisterVersion: 1 }); type AddysSummaryQueryKey = ReturnType; @@ -88,6 +88,5 @@ export function useAddysSummary( ...config, staleTime: 1000 * 60 * 2, // Set data to become stale after 2 minutes cacheTime: 1000 * 60 * 60 * 24, // Keep unused data in cache for 24 hours - keepPreviousData: true, // Use previous data while new data is loading after it becomes stale }); } diff --git a/src/screens/SignTransactionSheet.tsx b/src/screens/SignTransactionSheet.tsx index 84a0a3acdaa..d4b8b505431 100644 --- a/src/screens/SignTransactionSheet.tsx +++ b/src/screens/SignTransactionSheet.tsx @@ -57,7 +57,7 @@ import { import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; import { getAccountProfileInfo } from '@/helpers/accountInfo'; -import { useAccountSettings, useClipboard, useDimensions, useGas, useWallets } from '@/hooks'; +import { useAccountSettings, useClipboard, useDimensions, useGas, useSwitchWallet, useWallets } from '@/hooks'; import ImageAvatar from '@/components/contacts/ImageAvatar'; import { ContactAvatar } from '@/components/contacts'; import { IS_IOS } from '@/env'; @@ -162,7 +162,8 @@ export const SignTransactionSheet = () => { const [simulationScanResult, setSimulationScanResult] = useState(undefined); const { params: routeParams } = useRoute(); - const { wallets, walletNames, switchToWalletWithAddress } = useWallets(); + const { wallets, walletNames } = useWallets(); + const { switchToWalletWithAddress } = useSwitchWallet(); const { transactionDetails, onSuccess: onSuccessCallback, diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index c4c4a647676..02ea352ec5c 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -74,7 +74,7 @@ const WalletScreen: React.FC = ({ navigation, route }) => { }, [currentNetwork, revertToMainnet]); const walletReady = useSelector(({ appState: { walletReady } }: AppState) => walletReady); - const { isWalletEthZero, isLoadingUserAssets, briefSectionsData: walletBriefSectionsData } = useWalletSectionsData(); + const { isWalletEthZero, isLoadingUserAssets, isLoadingBalance, briefSectionsData: walletBriefSectionsData } = useWalletSectionsData(); useEffect(() => { // This is the fix for Android wallet creation problem. @@ -86,7 +86,7 @@ const WalletScreen: React.FC = ({ navigation, route }) => { if (isWelcomeScreen) { removeFirst(); } - }, [dangerouslyGetState, removeFirst]); + }, [dangerouslyGetParent, dangerouslyGetState, removeFirst]); useEffect(() => { const initializeAndSetParams = async () => { @@ -132,8 +132,8 @@ const WalletScreen: React.FC = ({ navigation, route }) => { {/* @ts-expect-error JavaScript component */}