From b8462185f8f3b67a3c7dbc4357800f21b09457d2 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:01:19 +0200 Subject: [PATCH] feat: revert of the revert of "fix: swapper transition states (#7917)" (#8002) --- .../SharedTradeInput/SharedTradeInputBody.tsx | 4 +- .../SharedTradeInputFooter.tsx | 7 +- .../TradeInput/components/ConfirmSummary.tsx | 11 +- .../components/ManualAddressEntry.tsx | 32 +-- .../TradeInput/components/WithLazyMount.tsx | 4 - src/context/AppProvider/AppContext.tsx | 198 +---------------- .../hooks/useAccountsFetchQuery.tsx | 205 ++++++++++++++++++ .../RFOX/components/Stake/StakeInput.tsx | 4 +- .../__snapshots__/portfolioSlice.test.ts.snap | 6 - .../slices/portfolioSlice/portfolioSlice.ts | 3 - .../portfolioSlice/portfolioSliceCommon.ts | 2 - src/state/slices/portfolioSlice/selectors.ts | 2 - src/test/mocks/store.ts | 1 - 13 files changed, 237 insertions(+), 242 deletions(-) create mode 100644 src/context/AppProvider/hooks/useAccountsFetchQuery.tsx diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx index ccb5165f5dd..a2c7e399190 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx @@ -15,6 +15,7 @@ import { useCallback, useEffect, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import { TradeAssetSelect } from 'components/AssetSelection/AssetSelection' import { useInputOutputDifferenceDecimalPercentage } from 'components/MultiHopTrade/hooks/useInputOutputDifference' +import { useAccountsFetchQuery } from 'context/AppProvider/hooks/useAccountsFetchQuery' import { useModal } from 'hooks/useModal/useModal' import { useWallet } from 'hooks/useWallet/useWallet' import { useWalletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' @@ -23,7 +24,6 @@ import { selectHasUserEnteredAmount, selectHighestMarketCapFeeAsset, selectIsAccountMetadataLoadingByAccountId, - selectIsAccountsMetadataLoading, selectWalletConnectedChainIds, } from 'state/slices/selectors' import { useAppSelector } from 'state/store' @@ -92,7 +92,7 @@ export const SharedTradeInputBody = ({ const walletConnectedChainIds = useAppSelector(selectWalletConnectedChainIds) const defaultSellAsset = useAppSelector(selectHighestMarketCapFeeAsset) const hasUserEnteredAmount = useAppSelector(selectHasUserEnteredAmount) - const isAccountsMetadataLoading = useAppSelector(selectIsAccountsMetadataLoading) + const { isFetching: isAccountsMetadataLoading } = useAccountsFetchQuery() const isAccountMetadataLoadingByAccountId = useAppSelector( selectIsAccountMetadataLoadingByAccountId, ) diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter.tsx index 6387950e26f..495c5f36894 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter.tsx @@ -16,9 +16,10 @@ import { ReceiveSummary } from 'components/MultiHopTrade/components/TradeInput/c import { RecipientAddress } from 'components/MultiHopTrade/components/TradeInput/components/RecipientAddress' import { WithLazyMount } from 'components/MultiHopTrade/components/TradeInput/components/WithLazyMount' import { Text } from 'components/Text' +import { useAccountsFetchQuery } from 'context/AppProvider/hooks/useAccountsFetchQuery' import { useWallet } from 'hooks/useWallet/useWallet' import { useWalletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' -import { selectFeeAssetById, selectIsAccountsMetadataLoading } from 'state/slices/selectors' +import { selectFeeAssetById } from 'state/slices/selectors' import { useAppSelector } from 'state/store' import { breakpoints } from 'theme/theme' @@ -91,7 +92,8 @@ export const SharedTradeInputFooter = ({ const buyAssetFeeAsset = useAppSelector(state => selectFeeAssetById(state, buyAsset?.assetId ?? ''), ) - const isAccountsMetadataLoading = useAppSelector(selectIsAccountsMetadataLoading) + + const { isFetching: isAccountsMetadataLoading } = useAccountsFetchQuery() const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) const displayManualAddressEntry = useMemo(() => { @@ -195,7 +197,6 @@ export const SharedTradeInputFooter = ({ shouldForceManualAddressEntry={shouldForceManualAddressEntry} component={ManualAddressEntry} description={manualAddressEntryDescription} - chainId={buyAsset.chainId} /> selectFeeAssetById(state, buyAsset?.assetId ?? ''), ) - const isAccountsMetadataLoading = useAppSelector(selectIsAccountsMetadataLoading) + const { isFetching: isAccountsMetadataLoading } = useAccountsFetchQuery() + const inputAmountUsd = useAppSelector(selectInputSellAmountUsd) // use the fee data from the actual quote in case it varies from the theoretical calculation const affiliateBps = useAppSelector(selectActiveQuoteAffiliateBps) @@ -102,8 +103,7 @@ export const ConfirmSummary = ({ const { data: _isSmartContractReceiveAddress, isLoading: isReceiveAddressByteCodeLoading } = useIsSmartContractAddress(receiveAddress ?? '', buyAsset.chainId) - - const { sellAssetAccountId } = useAccountIds() + const { sellAssetAccountId, buyAssetAccountId } = useAccountIds() const isTaprootReceiveAddress = useMemo( () => isUtxoChainId(buyAsset.chainId) && receiveAddress?.startsWith('bc1p'), @@ -185,7 +185,7 @@ export const ConfirmSummary = ({ const quoteResponseError = quoteResponseErrors[0] const tradeQuoteError = activeQuoteErrors?.[0] switch (true) { - case isAccountsMetadataLoading && !sellAssetAccountId: + case isAccountsMetadataLoading && !(sellAssetAccountId || buyAssetAccountId): return 'common.accountsLoading' case !shouldShowTradeQuoteOrAwaitInput: case !hasUserEnteredAmount: @@ -210,6 +210,7 @@ export const ConfirmSummary = ({ activeQuoteErrors, isAccountsMetadataLoading, sellAssetAccountId, + buyAssetAccountId, shouldShowTradeQuoteOrAwaitInput, hasUserEnteredAmount, isAnyTradeQuoteLoading, diff --git a/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx b/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx index 864e645f82c..8e923e98e47 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx @@ -1,5 +1,4 @@ import { FormControl, FormLabel, Link } from '@chakra-ui/react' -import { type ChainId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' import { isMetaMask } from '@shapeshiftoss/hdwallet-metamask-multichain' import type { FC } from 'react' @@ -9,32 +8,28 @@ import { useTranslate } from 'react-polyglot' import { AddressInput } from 'components/Modals/Send/AddressInput/AddressInput' import { SendFormFields } from 'components/Modals/Send/SendCommon' import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' +import { useAccountsFetchQuery } from 'context/AppProvider/hooks/useAccountsFetchQuery' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' import { useIsSnapInstalled } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { useModal } from 'hooks/useModal/useModal' import { useWallet } from 'hooks/useWallet/useWallet' import { useWalletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' import { parseAddressInputWithChainId } from 'lib/address/address' +import { selectAccountIdsByAssetId } from 'state/slices/selectors' import { - selectAccountIdsByAssetId, - selectIsAnyAccountMetadataLoadingForChainId, -} from 'state/slices/selectors' -import { selectInputBuyAsset } from 'state/slices/tradeInputSlice/selectors' + selectFirstHopSellAccountId, + selectInputBuyAsset, +} from 'state/slices/tradeInputSlice/selectors' import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { useAppDispatch, useAppSelector } from 'state/store' type ManualAddressEntryProps = { description?: string shouldForceManualAddressEntry?: boolean - chainId: ChainId } export const ManualAddressEntry: FC = memo( - ({ - description, - shouldForceManualAddressEntry, - chainId, - }: ManualAddressEntryProps): JSX.Element | null => { + ({ description, shouldForceManualAddressEntry }: ManualAddressEntryProps): JSX.Element | null => { const dispatch = useAppDispatch() const { @@ -61,19 +56,13 @@ export const ManualAddressEntry: FC = memo( }), [wallet], ) + const sellAssetAccountId = useAppSelector(selectFirstHopSellAccountId) const { manualReceiveAddress } = useReceiveAddress(useReceiveAddressArgs) - const isAnyAccountMetadataLoadingByChainIdFilter = useMemo(() => ({ chainId }), [chainId]) - const isAnyAccountMetadataLoadingByChainId = useAppSelector(state => - selectIsAnyAccountMetadataLoadingForChainId( - state, - isAnyAccountMetadataLoadingByChainIdFilter, - ), - ) + const { isFetching: isAccountsMetadataLoading } = useAccountsFetchQuery() const shouldShowManualReceiveAddressInput = useMemo(() => { - // Some AccountIds are loading for that chain - don't show the manual address input since these will eventually be populated - if (isAnyAccountMetadataLoadingByChainId) return false + if (isAccountsMetadataLoading && !sellAssetAccountId) return false if (shouldForceManualAddressEntry) return true if (manualReceiveAddress) return false // Ledger "supports" all chains, but may not have them connected @@ -81,7 +70,8 @@ export const ManualAddressEntry: FC = memo( // We want to display the manual address entry if the wallet doesn't support the buy asset chain return !walletSupportsBuyAssetChain }, [ - isAnyAccountMetadataLoadingByChainId, + isAccountsMetadataLoading, + sellAssetAccountId, shouldForceManualAddressEntry, manualReceiveAddress, wallet, diff --git a/src/components/MultiHopTrade/components/TradeInput/components/WithLazyMount.tsx b/src/components/MultiHopTrade/components/TradeInput/components/WithLazyMount.tsx index da2ca9f1893..cdca3f3b0b0 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/WithLazyMount.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/WithLazyMount.tsx @@ -25,10 +25,6 @@ export const WithLazyMount = (props: WithLazyRenderProps) = }, Object.values(props)) useEffect(() => { - if (!shouldUse || persistentShouldUse.current === true) { - return - } - persistentShouldUse.current = shouldUse }, [shouldUse]) diff --git a/src/context/AppProvider/AppContext.tsx b/src/context/AppProvider/AppContext.tsx index fc643027dc3..07aadc48371 100644 --- a/src/context/AppProvider/AppContext.tsx +++ b/src/context/AppProvider/AppContext.tsx @@ -1,11 +1,6 @@ import { usePrevious, useToast } from '@chakra-ui/react' -import type { AccountId, ChainId } from '@shapeshiftoss/caip' -import { fromAccountId } from '@shapeshiftoss/caip' import type { LedgerOpenAppEventArgs } from '@shapeshiftoss/chain-adapters' import { emitter } from '@shapeshiftoss/chain-adapters' -import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' -import type { AccountMetadataById } from '@shapeshiftoss/types' import { useQueries } from '@tanstack/react-query' import { DEFAULT_HISTORY_TIMEFRAME } from 'constants/Config' import { LanguageTypeEnum } from 'constants/LanguageTypeEnum' @@ -19,8 +14,6 @@ import { useModal } from 'hooks/useModal/useModal' import { useRouteAssetId } from 'hooks/useRouteAssetId/useRouteAssetId' import { useWallet } from 'hooks/useWallet/useWallet' import { walletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' -import { deriveAccountIdsAndMetadata } from 'lib/account/account' -import { isUtxoChainId } from 'lib/utils/utxo' import { snapshotApi } from 'state/apis/snapshot/snapshot' import { useGetAssetsQuery } from 'state/slices/assetsSlice/assetsSlice' import { @@ -28,12 +21,11 @@ import { marketData, useFindAllQuery, } from 'state/slices/marketDataSlice/marketDataSlice' -import { portfolio, portfolioApi } from 'state/slices/portfolioSlice/portfolioSlice' +import { portfolio } from 'state/slices/portfolioSlice/portfolioSlice' import { preferences } from 'state/slices/preferencesSlice/preferencesSlice' import { selectAccountIdsByChainId, selectAssetIds, - selectEnabledWalletAccountIds, selectPortfolioAssetIds, selectPortfolioLoadingStatus, selectSelectedCurrency, @@ -41,9 +33,10 @@ import { selectWalletId, } from 'state/slices/selectors' import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' -import { txHistoryApi } from 'state/slices/txHistorySlice/txHistorySlice' import { useAppDispatch, useAppSelector } from 'state/store' +import { useAccountsFetchQuery } from './hooks/useAccountsFetchQuery' + /** * note - be super careful playing with this component, as it's responsible for asset, * market data, and portfolio fetching, and we don't want to over or under fetch data, @@ -61,14 +54,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const { supportedChains } = usePlugins() const { wallet, isConnected } = useWallet().state const assetIds = useAppSelector(selectAssetIds) - const requestedAccountIds = useAppSelector(selectEnabledWalletAccountIds) const portfolioLoadingStatus = useAppSelector(selectPortfolioLoadingStatus) const portfolioAssetIds = useAppSelector(selectPortfolioAssetIds) const walletId = useAppSelector(selectWalletId) const prevWalletId = usePrevious(walletId) const routeAssetId = useRouteAssetId() const { isSnapInstalled } = useIsSnapInstalled() - const previousIsSnapInstalled = usePrevious(isSnapInstalled) const { close: closeModal, open: openModal } = useModal('ledgerOpenApp') useEffect(() => { @@ -110,6 +101,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { // and covers most assets users will have useFindAllQuery() + // Master hook for accounts fetch as a react-query + useAccountsFetchQuery() + const selectedLocale = useAppSelector(selectSelectedLocale) useEffect(() => { if (selectedLocale in LanguageTypeEnum ?? {}) { @@ -118,6 +112,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { }, [selectedLocale]) const accountIdsByChainId = useAppSelector(selectAccountIdsByChainId) + useEffect(() => { if (!wallet) return const walletSupportedChainIds = supportedChains.filter(chainId => { @@ -131,185 +126,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { dispatch(portfolio.actions.setWalletSupportedChainIds(walletSupportedChainIds)) }, [accountIdsByChainId, dispatch, isSnapInstalled, wallet, supportedChains]) - // Initial account and portfolio fetch for non-ledger wallets - useEffect(() => { - const hasManagedAccounts = (() => { - // MM without snap doesn't allow account management - if the user just installed the snap, we know they don't have managed accounts - if (!previousIsSnapInstalled && isSnapInstalled) return false - // We know snap wasn't just installed in this render - so if there are any requestedAccountIds, we assume the user has managed accounts - return requestedAccountIds.length > 0 - })() - - let chainIds = new Set( - supportedChains.filter(chainId => { - return walletSupportsChain({ - chainId, - wallet, - isSnapInstalled, - checkConnectedAccountIds: false, // don't check connected account ids, we're detecting runtime support for chains - }) - }), - ) - if (!chainIds.size) return - ;(async () => { - dispatch(portfolio.actions.setIsAccountsMetadataLoading(true)) - - // Fetch portfolio for all managed accounts if they exist instead of going through the initial account detection flow. - // This ensures that we have fresh portfolio data, but accounts added through account management are not accidentally blown away. - if (hasManagedAccounts) { - requestedAccountIds.forEach(accountId => { - dispatch(portfolioApi.endpoints.getAccount.initiate({ accountId, upsertOnFetch: true })) - }) - - return - } - - if (!wallet || isLedger(wallet)) return - - const walletId = await wallet.getDeviceID() - - const accountMetadataByAccountId: AccountMetadataById = {} - const isMultiAccountWallet = wallet.supportsBip44Accounts() - const isMetaMaskMultichainWallet = wallet instanceof MetaMaskMultiChainHDWallet - for (let accountNumber = 0; chainIds.size > 0; accountNumber++) { - if ( - accountNumber > 0 && - // only some wallets support multi account - (!isMultiAccountWallet || - // MM without snaps does not support non-EVM chains, hence no multi-account - // since EVM chains in MM use MetaMask's native JSON-RPC functionality which doesn't support multi-account - (isMetaMaskMultichainWallet && !isSnapInstalled)) - ) - break - - const input = { - accountNumber, - chainIds: Array.from(chainIds), - wallet, - isSnapInstalled: Boolean(isSnapInstalled), - } - const accountIdsAndMetadata = await deriveAccountIdsAndMetadata(input) - const accountIds = Object.keys(accountIdsAndMetadata) - - Object.assign(accountMetadataByAccountId, accountIdsAndMetadata) - - const { getAccount } = portfolioApi.endpoints - - const accountNumberAccountIdsByChainId = ( - _accountIds: AccountId[], - ): Record => { - return _accountIds.reduce( - (acc, _accountId) => { - const { chainId } = fromAccountId(_accountId) - - if (!acc[chainId]) { - acc[chainId] = [] - } - acc[chainId].push(_accountId) - - return acc - }, - {} as Record, - ) - } - - let chainIdsWithActivity: Set = new Set() - // This allows every run of AccountIds per chain/accountNumber to run in parallel vs. all sequentally, so - // we can run each item (usually one AccountId, except UTXOs which may contain many because of many scriptTypes) 's side effects immediately - const accountNumberAccountIdsPromises = Object.values( - accountNumberAccountIdsByChainId(accountIds), - ).map(async accountIds => { - const results = await Promise.allSettled( - accountIds.map(async id => { - const result = await dispatch(getAccount.initiate({ accountId: id })) - return result - }), - ) - - results.forEach((res, idx) => { - if (res.status === 'rejected') return - - const { data: account } = res.value - if (!account) return - - const accountId = accountIds[idx] - const { chainId } = fromAccountId(accountId) - - const { hasActivity } = account.accounts.byId[accountId] - - const accountNumberHasChainActivity = !isUtxoChainId(chainId) - ? hasActivity - : // For UTXO AccountIds, we need to check if *any* of the scriptTypes have activity, not only the current one - // else, we might end up with partial account data, with only the first 1 or 2 out of 3 scriptTypes - // being upserted for BTC and LTC - results.some((res, _idx) => { - if (res.status === 'rejected') return false - const { data: account } = res.value - if (!account) return false - const accountId = accountIds[_idx] - const { chainId: _chainId } = fromAccountId(accountId) - if (chainId !== _chainId) return false - return account.accounts.byId[accountId].hasActivity - }) - - // don't add accounts with no activity past account 0 - if (accountNumber > 0 && !accountNumberHasChainActivity) { - chainIdsWithActivity.delete(chainId) - delete accountMetadataByAccountId[accountId] - } else { - // handle utxo chains with multiple account types per account - chainIdsWithActivity.add(chainId) - - dispatch(portfolio.actions.upsertPortfolio(account)) - const chainIdAccountMetadata = Object.entries(accountMetadataByAccountId).reduce( - (acc, [accountId, metadata]) => { - const { chainId: _chainId } = fromAccountId(accountId) - if (chainId === _chainId) { - acc[accountId] = metadata - } - return acc - }, - {} as AccountMetadataById, - ) - dispatch( - portfolio.actions.upsertAccountMetadata({ - accountMetadataByAccountId: chainIdAccountMetadata, - walletId, - }), - ) - for (const accountId of Object.keys(accountMetadataByAccountId)) { - dispatch(portfolio.actions.enableAccountId(accountId)) - } - } - }) - - return results - }) - - await Promise.allSettled(accountNumberAccountIdsPromises) - chainIds = chainIdsWithActivity - } - })().then(async () => { - dispatch(portfolio.actions.setIsAccountsMetadataLoading(false)) - // Only fetch and upsert Tx history once all are loaded, otherwise big main thread rug - const { getAllTxHistory } = txHistoryApi.endpoints - - await Promise.all( - requestedAccountIds.map(requestedAccountId => - dispatch(getAllTxHistory.initiate(requestedAccountId)), - ), - ) - }) - }, [ - dispatch, - wallet, - supportedChains, - isSnapInstalled, - requestedAccountIds.length, - previousIsSnapInstalled, - requestedAccountIds, - ]) - useEffect(() => { if (portfolioLoadingStatus === 'loading') return if (!isConnected) return diff --git a/src/context/AppProvider/hooks/useAccountsFetchQuery.tsx b/src/context/AppProvider/hooks/useAccountsFetchQuery.tsx new file mode 100644 index 00000000000..8c7d827871b --- /dev/null +++ b/src/context/AppProvider/hooks/useAccountsFetchQuery.tsx @@ -0,0 +1,205 @@ +import { usePrevious } from '@chakra-ui/react' +import type { AccountId, ChainId } from '@shapeshiftoss/caip' +import { fromAccountId } from '@shapeshiftoss/caip' +import { isLedger } from '@shapeshiftoss/hdwallet-ledger' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' +import type { AccountMetadataById } from '@shapeshiftoss/types' +import { skipToken, useQuery } from '@tanstack/react-query' +import { useCallback, useEffect, useMemo } from 'react' +import { usePlugins } from 'context/PluginProvider/PluginProvider' +import { useIsSnapInstalled } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' +import { useWallet } from 'hooks/useWallet/useWallet' +import { walletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' +import { deriveAccountIdsAndMetadata } from 'lib/account/account' +import { isUtxoChainId } from 'lib/utils/utxo' +import { portfolio, portfolioApi } from 'state/slices/portfolioSlice/portfolioSlice' +import { selectEnabledWalletAccountIds } from 'state/slices/selectors' +import { txHistoryApi } from 'state/slices/txHistorySlice/txHistorySlice' +import { useAppDispatch, useAppSelector } from 'state/store' + +export const useAccountsFetchQuery = () => { + const dispatch = useAppDispatch() + const enabledWalletAccountIds = useAppSelector(selectEnabledWalletAccountIds) + const { supportedChains } = usePlugins() + const { isSnapInstalled } = useIsSnapInstalled() + const previousIsSnapInstalled = usePrevious(isSnapInstalled) + const { deviceId, wallet } = useWallet().state + + const hasManagedAccounts = useMemo(() => { + // MM without snap doesn't allow account management - if the user just installed the snap, we know they don't have managed accounts + if (!previousIsSnapInstalled && isSnapInstalled) return false + // We know snap wasn't just installed in this render - so if there are any requestedAccountIds, we assume the user has managed accounts + return enabledWalletAccountIds.length > 0 + }, [isSnapInstalled, previousIsSnapInstalled, enabledWalletAccountIds.length]) + + // Fetch portfolio for all managed accounts as a side-effect if they exist instead of going through the initial account detection flow. + // This ensures that we have fresh portfolio data, but accounts added through account management are not accidentally blown away. + useEffect(() => { + const { getAllTxHistory } = txHistoryApi.endpoints + + // Note no force refetch here - only fetch Tx history once per acccount + enabledWalletAccountIds.forEach(accountId => { + dispatch(portfolioApi.endpoints.getAccount.initiate({ accountId, upsertOnFetch: true })) + }) + + // Note no force refetch here - only fetch Tx history once per acccount + enabledWalletAccountIds.map(requestedAccountId => + dispatch(getAllTxHistory.initiate(requestedAccountId)), + ) + }, [dispatch, enabledWalletAccountIds]) + + const queryFn = useCallback(async () => { + let chainIds = new Set( + supportedChains.filter(chainId => { + return walletSupportsChain({ + chainId, + wallet, + isSnapInstalled, + checkConnectedAccountIds: false, // don't check connected account ids, we're detecting runtime support for chains + }) + }), + ) + if (!chainIds.size) return + + if (!wallet || isLedger(wallet)) return + + const walletId = await wallet.getDeviceID() + + const accountMetadataByAccountId: AccountMetadataById = {} + const isMultiAccountWallet = wallet.supportsBip44Accounts() + const isMetaMaskMultichainWallet = wallet instanceof MetaMaskMultiChainHDWallet + for (let accountNumber = 0; chainIds.size > 0; accountNumber++) { + if ( + accountNumber > 0 && + // only some wallets support multi account + (!isMultiAccountWallet || + // MM without snaps does not support non-EVM chains, hence no multi-account + // since EVM chains in MM use MetaMask's native JSON-RPC functionality which doesn't support multi-account + (isMetaMaskMultichainWallet && !isSnapInstalled)) + ) + break + + const input = { + accountNumber, + chainIds: Array.from(chainIds), + wallet, + isSnapInstalled: Boolean(isSnapInstalled), + } + const accountIdsAndMetadata = await deriveAccountIdsAndMetadata(input) + const accountIds = Object.keys(accountIdsAndMetadata) + + Object.assign(accountMetadataByAccountId, accountIdsAndMetadata) + + const { getAccount } = portfolioApi.endpoints + + const accountNumberAccountIdsByChainId = ( + _accountIds: AccountId[], + ): Record => { + return _accountIds.reduce( + (acc, _accountId) => { + const { chainId } = fromAccountId(_accountId) + + if (!acc[chainId]) { + acc[chainId] = [] + } + acc[chainId].push(_accountId) + + return acc + }, + {} as Record, + ) + } + + let chainIdsWithActivity: Set = new Set() + // This allows every run of AccountIds per chain/accountNumber to run in parallel vs. all sequentally, so + // we can run each item (usually one AccountId, except UTXOs which may contain many because of many scriptTypes) 's side effects immediately + const accountNumberAccountIdsPromises = Object.values( + accountNumberAccountIdsByChainId(accountIds), + ).map(async accountIds => { + const results = await Promise.allSettled( + accountIds.map(async id => { + const result = await dispatch( + getAccount.initiate({ accountId: id, upsertOnFetch: true }), + ) + return result + }), + ) + + results.forEach((res, idx) => { + if (res.status === 'rejected') return + + const { data: account } = res.value + if (!account) return + + const accountId = accountIds[idx] + const { chainId } = fromAccountId(accountId) + + const { hasActivity } = account.accounts.byId[accountId] + + const accountNumberHasChainActivity = !isUtxoChainId(chainId) + ? hasActivity + : // For UTXO AccountIds, we need to check if *any* of the scriptTypes have activity, not only the current one + // else, we might end up with partial account data, with only the first 1 or 2 out of 3 scriptTypes + // being upserted for BTC and LTC + results.some((res, _idx) => { + if (res.status === 'rejected') return false + const { data: account } = res.value + if (!account) return false + const accountId = accountIds[_idx] + const { chainId: _chainId } = fromAccountId(accountId) + if (chainId !== _chainId) return false + return account.accounts.byId[accountId].hasActivity + }) + + // don't add accounts with no activity past account 0 + if (accountNumber > 0 && !accountNumberHasChainActivity) { + chainIdsWithActivity.delete(chainId) + delete accountMetadataByAccountId[accountId] + } else { + // handle utxo chains with multiple account types per account + chainIdsWithActivity.add(chainId) + + dispatch(portfolio.actions.upsertPortfolio(account)) + const chainIdAccountMetadata = Object.entries(accountMetadataByAccountId).reduce( + (acc, [accountId, metadata]) => { + const { chainId: _chainId } = fromAccountId(accountId) + if (chainId === _chainId) { + acc[accountId] = metadata + } + return acc + }, + {} as AccountMetadataById, + ) + for (const accountId of Object.keys(chainIdAccountMetadata)) { + dispatch(portfolio.actions.enableAccountId(accountId)) + } + dispatch( + portfolio.actions.upsertAccountMetadata({ + accountMetadataByAccountId: chainIdAccountMetadata, + walletId, + }), + ) + } + }) + + return results + }) + + await Promise.allSettled(accountNumberAccountIdsPromises) + chainIds = chainIdsWithActivity + } + }, [dispatch, isSnapInstalled, supportedChains, wallet]) + + const query = useQuery({ + queryKey: [ + 'useAccountsFetch', + { + deviceId, + supportedChains, + }, + ], + queryFn: deviceId && !hasManagedAccounts ? queryFn : skipToken, + }) + + return query +} diff --git a/src/pages/RFOX/components/Stake/StakeInput.tsx b/src/pages/RFOX/components/Stake/StakeInput.tsx index b3ec083218e..e1dd3a252fe 100644 --- a/src/pages/RFOX/components/Stake/StakeInput.tsx +++ b/src/pages/RFOX/components/Stake/StakeInput.tsx @@ -21,6 +21,7 @@ import { getChainShortName } from 'components/MultiHopTrade/components/MultiHopT import { TradeAssetInput } from 'components/MultiHopTrade/components/TradeAssetInput' import { Row } from 'components/Row/Row' import { SlideTransition } from 'components/SlideTransition' +import { useAccountsFetchQuery } from 'context/AppProvider/hooks/useAccountsFetchQuery' import { useModal } from 'hooks/useModal/useModal' import { useToggle } from 'hooks/useToggle/useToggle' import { useWallet } from 'hooks/useWallet/useWallet' @@ -35,7 +36,6 @@ import { marketApi } from 'state/slices/marketDataSlice/marketDataSlice' import { selectAssetById, selectFeeAssetByChainId, - selectIsAccountsMetadataLoading, selectMarketDataByAssetIdUserCurrency, selectMarketDataByFilter, selectPortfolioCryptoPrecisionBalanceByFilter, @@ -95,7 +95,7 @@ export const StakeInput: React.FC = ({ select: selectRuneAddress, }) - const isAccountsMetadataLoading = useAppSelector(selectIsAccountsMetadataLoading) + const { isFetching: isAccountsMetadataLoading } = useAccountsFetchQuery() const isBridgeRequired = stakingAssetId !== selectedAssetId const dispatch = useAppDispatch() const translate = useTranslate() diff --git a/src/state/slices/portfolioSlice/__snapshots__/portfolioSlice.test.ts.snap b/src/state/slices/portfolioSlice/__snapshots__/portfolioSlice.test.ts.snap index 6761414e2b6..8aec66f07c9 100644 --- a/src/state/slices/portfolioSlice/__snapshots__/portfolioSlice.test.ts.snap +++ b/src/state/slices/portfolioSlice/__snapshots__/portfolioSlice.test.ts.snap @@ -31,7 +31,6 @@ exports[`portfolioSlice > reducers > upsertPortfolio > Bitcoin > should update s }, "enabledAccountIds": {}, "isAccountMetadataLoadingByAccountId": {}, - "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -81,7 +80,6 @@ exports[`portfolioSlice > reducers > upsertPortfolio > Bitcoin > should update s }, "enabledAccountIds": {}, "isAccountMetadataLoadingByAccountId": {}, - "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -154,7 +152,6 @@ exports[`portfolioSlice > reducers > upsertPortfolio > Ethereum and bitcoin > sh }, "enabledAccountIds": {}, "isAccountMetadataLoadingByAccountId": {}, - "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -212,7 +209,6 @@ exports[`portfolioSlice > reducers > upsertPortfolio > Ethereum and bitcoin > sh }, "enabledAccountIds": {}, "isAccountMetadataLoadingByAccountId": {}, - "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -253,7 +249,6 @@ exports[`portfolioSlice > reducers > upsertPortfolio > ethereum > should update }, "enabledAccountIds": {}, "isAccountMetadataLoadingByAccountId": {}, - "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -307,7 +302,6 @@ exports[`portfolioSlice > reducers > upsertPortfolio > ethereum > should update }, "enabledAccountIds": {}, "isAccountMetadataLoadingByAccountId": {}, - "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], diff --git a/src/state/slices/portfolioSlice/portfolioSlice.ts b/src/state/slices/portfolioSlice/portfolioSlice.ts index 57d46633546..09a08a4b5b5 100644 --- a/src/state/slices/portfolioSlice/portfolioSlice.ts +++ b/src/state/slices/portfolioSlice/portfolioSlice.ts @@ -32,9 +32,6 @@ export const portfolio = createSlice({ clear: () => { return initialState }, - setIsAccountsMetadataLoading: (state, { payload }: { payload: boolean }) => { - state.isAccountsMetadataLoading = payload - }, setIsAccountMetadataLoading: ( state, { payload }: { payload: { accountId: AccountId; isLoading: boolean } }, diff --git a/src/state/slices/portfolioSlice/portfolioSliceCommon.ts b/src/state/slices/portfolioSlice/portfolioSliceCommon.ts index d3a2be600a1..04af2ef4fd3 100644 --- a/src/state/slices/portfolioSlice/portfolioSliceCommon.ts +++ b/src/state/slices/portfolioSlice/portfolioSliceCommon.ts @@ -54,7 +54,6 @@ export type ConnectWallet = { } export type Portfolio = { - isAccountsMetadataLoading: boolean isAccountMetadataLoadingByAccountId: Record /** * lookup of accountId -> accountMetadata @@ -75,7 +74,6 @@ export type Portfolio = { } export const initialState: Portfolio = { - isAccountsMetadataLoading: false, isAccountMetadataLoadingByAccountId: {}, accounts: { byId: {}, diff --git a/src/state/slices/portfolioSlice/selectors.ts b/src/state/slices/portfolioSlice/selectors.ts index 70f5a3f9fe5..c5af5d2b161 100644 --- a/src/state/slices/portfolioSlice/selectors.ts +++ b/src/state/slices/portfolioSlice/selectors.ts @@ -1155,8 +1155,6 @@ export const selectWalletConnectedChainIdsSorted = createDeepEqualOutputSelector }, ) -export const selectIsAccountsMetadataLoading = (state: ReduxState) => - state.portfolio.isAccountsMetadataLoading export const selectIsAccountMetadataLoadingByAccountId = (state: ReduxState) => state.portfolio.isAccountMetadataLoadingByAccountId export const selectIsAnyAccountMetadataLoadingForChainId = createSelector( diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index a3ef4d1979f..2c70040a685 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -46,7 +46,6 @@ export const mockStore: ReduxState = { version: 0, rehydrated: false, }, - isAccountsMetadataLoading: false, isAccountMetadataLoadingByAccountId: {}, accounts: { byId: {},