From 144b1c445e42b695e863086fc07845fafdcce34e Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 9 Oct 2024 22:53:06 +0200 Subject: [PATCH] fix: swapper uses default asset instead of default default assets while accounts are loading (#7896) --- .../components/TradeInput/TradeInput.tsx | 17 +- .../TradeInput/components/ConfirmSummary.tsx | 29 +- .../components/ManualAddressEntry.tsx | 31 +- .../components/RecipientAddress.tsx | 5 +- .../TradeInput/components/TradeInputBody.tsx | 19 +- src/context/AppProvider/AppContext.tsx | 297 +++++++++--------- .../RFOX/components/Stake/StakeInput.tsx | 16 +- .../__snapshots__/portfolioSlice.test.ts.snap | 18 +- .../slices/portfolioSlice/portfolioSlice.ts | 13 +- .../portfolioSlice/portfolioSliceCommon.ts | 6 +- src/state/slices/portfolioSlice/selectors.ts | 24 +- src/test/mocks/store.ts | 3 +- 12 files changed, 282 insertions(+), 196 deletions(-) diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index a7c9a897e6a..0f3907bb17b 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -29,7 +29,7 @@ import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis import { selectHasUserEnteredAmount, selectInputSellAsset, - selectIsAccountMetadataLoading, + selectIsAnyAccountMetadataLoadedForChainId, } from 'state/slices/selectors' import { selectActiveQuote, @@ -79,12 +79,18 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => { const [shouldShowArbitrumBridgeAcknowledgement, setShouldShowArbitrumBridgeAcknowledgement] = useState(false) const isKeplr = useMemo(() => !!wallet && isKeplrHDWallet(wallet), [wallet]) - const isAccountMetadataLoading = useAppSelector(selectIsAccountMetadataLoading) - const sellAsset = useAppSelector(selectInputSellAsset) const tradeQuoteStep = useAppSelector(selectFirstHop) const isUnsafeQuote = useAppSelector(selectIsUnsafeActiveQuote) + const isAnyAccountMetadataLoadedForChainIdFilter = useMemo( + () => ({ chainId: sellAsset.chainId }), + [sellAsset.chainId], + ) + const isAnyAccountMetadataLoadedForChainId = useAppSelector(state => + selectIsAnyAccountMetadataLoadedForChainId(state, isAnyAccountMetadataLoadedForChainIdFilter), + ) + const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput) const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted) const hasUserEnteredAmount = useAppSelector(selectHasUserEnteredAmount) @@ -116,7 +122,8 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => { const isLoading = useMemo( () => - isAccountMetadataLoading || + // No account meta loaded for that chain + !isAnyAccountMetadataLoadedForChainId || (!shouldShowTradeQuoteOrAwaitInput && !isTradeQuoteRequestAborted) || isConfirmationLoading || // Only consider snapshot API queries as pending if we don't have voting power yet @@ -124,11 +131,11 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => { // as we are optimistic and don't want to be waiting for a potentially very long time for the snapshot API to respond isVotingPowerLoading, [ + isAnyAccountMetadataLoadedForChainId, shouldShowTradeQuoteOrAwaitInput, isTradeQuoteRequestAborted, isConfirmationLoading, isVotingPowerLoading, - isAccountMetadataLoading, ], ) diff --git a/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx b/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx index 908346d1efb..8a620355de4 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx @@ -8,6 +8,7 @@ import { useTranslate } from 'react-polyglot' import { useHistory } from 'react-router' import { ButtonWalletPredicate } from 'components/ButtonWalletPredicate/ButtonWalletPredicate' import { usePriceImpact } from 'components/MultiHopTrade/hooks/quoteValidation/usePriceImpact' +import { useAccountIds } from 'components/MultiHopTrade/hooks/useAccountIds' import { TradeRoutePaths } from 'components/MultiHopTrade/types' import { Text } from 'components/Text' import { useIsSmartContractAddress } from 'hooks/useIsSmartContractAddress/useIsSmartContractAddress' @@ -20,7 +21,7 @@ import { selectHasUserEnteredAmount, selectInputBuyAsset, selectInputSellAsset, - selectIsAccountMetadataLoading, + selectIsAccountsMetadataLoading, selectManualReceiveAddressIsEditing, selectManualReceiveAddressIsValid, selectManualReceiveAddressIsValidating, @@ -96,7 +97,7 @@ export const ConfirmSummary = ({ const buyAssetFeeAsset = useAppSelector(state => selectFeeAssetById(state, buyAsset?.assetId ?? ''), ) - const isAccountMetadataLoading = useAppSelector(selectIsAccountMetadataLoading) + const isAccountsMetadataLoading = useAppSelector(selectIsAccountsMetadataLoading) const { priceImpactPercentage } = usePriceImpact(activeQuote) const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) @@ -104,6 +105,8 @@ export const ConfirmSummary = ({ const { data: _isSmartContractReceiveAddress, isLoading: isReceiveAddressByteCodeLoading } = useIsSmartContractAddress(receiveAddress ?? '', buyAsset.chainId) + const { sellAssetAccountId: initialSellAssetAccountId } = useAccountIds() + const isTaprootReceiveAddress = useMemo( () => isUtxoChainId(buyAsset.chainId) && receiveAddress?.startsWith('bc1p'), [buyAsset.chainId, receiveAddress], @@ -137,12 +140,17 @@ export const ConfirmSummary = ({ ]) const displayManualAddressEntry = useMemo(() => { - if (isAccountMetadataLoading) return false + if (isAccountsMetadataLoading && !initialSellAssetAccountId) return false if (!walletSupportsBuyAssetChain) return true if (disableThorNativeSmartContractReceive) return true return false - }, [isAccountMetadataLoading, walletSupportsBuyAssetChain, disableThorNativeSmartContractReceive]) + }, [ + isAccountsMetadataLoading, + initialSellAssetAccountId, + walletSupportsBuyAssetChain, + disableThorNativeSmartContractReceive, + ]) const quoteHasError = useMemo(() => { if (!shouldShowTradeQuoteOrAwaitInput) return false @@ -163,7 +171,7 @@ export const ConfirmSummary = ({ const shouldDisablePreviewButton = useMemo(() => { return ( - isAccountMetadataLoading || + (isAccountsMetadataLoading && !initialSellAssetAccountId) || // don't allow executing a quote with errors quoteHasError || // don't execute trades while address is validating @@ -185,7 +193,8 @@ export const ConfirmSummary = ({ isTradeQuoteApiQueryPending[activeSwapperName] ) }, [ - isAccountMetadataLoading, + isAccountsMetadataLoading, + initialSellAssetAccountId, quoteHasError, manualReceiveAddressIsValidating, manualReceiveAddressIsEditing, @@ -204,7 +213,7 @@ export const ConfirmSummary = ({ const quoteResponseError = quoteResponseErrors[0] const tradeQuoteError = activeQuoteErrors?.[0] switch (true) { - case isAccountMetadataLoading: + case isAccountsMetadataLoading && !initialSellAssetAccountId: return 'common.accountsLoading' case !shouldShowTradeQuoteOrAwaitInput: case !hasUserEnteredAmount: @@ -227,7 +236,8 @@ export const ConfirmSummary = ({ quoteRequestErrors, quoteResponseErrors, activeQuoteErrors, - isAccountMetadataLoading, + isAccountsMetadataLoading, + initialSellAssetAccountId, shouldShowTradeQuoteOrAwaitInput, hasUserEnteredAmount, isAnyTradeQuoteLoading, @@ -342,10 +352,11 @@ export const ConfirmSummary = ({ shouldForceManualAddressEntry={disableThorNativeSmartContractReceive} component={ManualAddressEntry} description={manualAddressEntryDescription} + chainId={buyAsset.chainId} /> = memo( - ({ description, shouldForceManualAddressEntry }: ManualAddressEntryProps): JSX.Element | null => { + ({ + description, + shouldForceManualAddressEntry, + chainId, + }: ManualAddressEntryProps): JSX.Element | null => { const dispatch = useAppDispatch() - const isAccountMetadataLoading = useAppSelector(selectIsAccountMetadataLoading) const { formState: { isValidating }, @@ -57,8 +65,17 @@ export const ManualAddressEntry: FC = memo( ) const { manualReceiveAddress } = useReceiveAddress(useReceiveAddressArgs) + const isAnyAccountMetadataLoadingByChainIdFilter = useMemo(() => ({ chainId }), [chainId]) + const isAnyAccountMetadataLoadingByChainId = useAppSelector(state => + selectIsAnyAccountMetadataLoadingForChainId( + state, + isAnyAccountMetadataLoadingByChainIdFilter, + ), + ) + const shouldShowManualReceiveAddressInput = useMemo(() => { - if (isAccountMetadataLoading) return false + // 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 (shouldForceManualAddressEntry) return true if (manualReceiveAddress) return false // Ledger "supports" all chains, but may not have them connected @@ -66,12 +83,12 @@ 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 }, [ - buyAssetAccountIds.length, + isAnyAccountMetadataLoadingByChainId, + shouldForceManualAddressEntry, manualReceiveAddress, wallet, + buyAssetAccountIds.length, walletSupportsBuyAssetChain, - shouldForceManualAddressEntry, - isAccountMetadataLoading, ]) const chainAdapterManager = getChainAdapterManager() diff --git a/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx b/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx index a2b204a63fb..c7dbd9f43e9 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx @@ -25,7 +25,7 @@ import type { TextPropTypes } from 'components/Text/Text' import { useWallet } from 'hooks/useWallet/useWallet' import { parseAddressInputWithChainId } from 'lib/address/address' import { middleEllipsis } from 'lib/utils' -import { selectInputBuyAsset, selectIsAccountMetadataLoading } from 'state/slices/selectors' +import { selectInputBuyAsset } from 'state/slices/selectors' import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { useAppDispatch, useAppSelector } from 'state/store' @@ -44,7 +44,6 @@ export const RecipientAddress: React.FC = ({ }) => { const translate = useTranslate() const dispatch = useAppDispatch() - const isAccountMetadataLoading = useAppSelector(selectIsAccountMetadataLoading) const wallet = useWallet().state.wallet const useReceiveAddressArgs = useMemo( () => ({ @@ -157,7 +156,7 @@ export const RecipientAddress: React.FC = ({ const handleFormSubmit = useMemo(() => handleSubmit(onSubmit), [handleSubmit, onSubmit]) - if (!receiveAddress || shouldForceManualAddressEntry || isAccountMetadataLoading) return null + if (!receiveAddress || shouldForceManualAddressEntry) return null return isRecipientAddressEditing ? (
diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeInputBody.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeInputBody.tsx index b8c2e8f2acf..87b823e4a58 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/TradeInputBody.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeInputBody.tsx @@ -23,6 +23,8 @@ import { selectHighestMarketCapFeeAsset, selectInputBuyAsset, selectInputSellAsset, + selectIsAccountMetadataLoadingByAccountId, + selectIsAccountsMetadataLoading, selectWalletConnectedChainIds, } from 'state/slices/selectors' import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' @@ -67,6 +69,10 @@ export const TradeInputBody = ({ state: { wallet }, } = useWallet() + const isAccountMetadataLoadingByAccountId = useAppSelector( + selectIsAccountMetadataLoadingByAccountId, + ) + const isAccountsMetadataLoading = useAppSelector(selectIsAccountsMetadataLoading) const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectBuyAmountAfterFeesCryptoPrecision) const buyAmountAfterFeesUserCurrency = useAppSelector(selectBuyAmountAfterFeesUserCurrency) const walletConnectedChainIds = useAppSelector(selectWalletConnectedChainIds) @@ -104,12 +110,23 @@ export const TradeInputBody = ({ // If the user disconnects the chain for the currently selected sell asset, switch to the default asset useEffect(() => { + // Don't do any default asset business as some accounts meta is still loading, or a wrong default asset may be set, + // which takes over the "default default" sellAsset - double default intended: + // https://github.com/shapeshift/web/blob/ba43c41527156f8c7e0f1170472ff362e091b450/src/state/slices/tradeInputSlice/tradeInputSlice.ts#L27 + if (Object.values(isAccountMetadataLoadingByAccountId).some(Boolean)) return if (!defaultSellAsset) return if (walletConnectedChainIds.includes(sellAsset.chainId)) return setSellAsset(defaultSellAsset) - }, [defaultSellAsset, sellAsset, setSellAsset, walletConnectedChainIds]) + }, [ + defaultSellAsset, + isAccountMetadataLoadingByAccountId, + isAccountsMetadataLoading, + sellAsset, + setSellAsset, + walletConnectedChainIds, + ]) const handleSellAssetClick = useCallback(() => { sellAssetSearch.open({ diff --git a/src/context/AppProvider/AppContext.tsx b/src/context/AppProvider/AppContext.tsx index 2ef9748440f..e0d7add9a74 100644 --- a/src/context/AppProvider/AppContext.tsx +++ b/src/context/AppProvider/AppContext.tsx @@ -137,169 +137,166 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { 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 () => { - try { - dispatch(portfolio.actions.setIsAccountMetadataLoading(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 })) - }) + 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 MetaMaskShapeShiftMultiChainHDWallet + 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 - return + const input = { + accountNumber, + chainIds: Array.from(chainIds), + wallet, + isSnapInstalled: Boolean(isSnapInstalled), } + const accountIdsAndMetadata = await deriveAccountIdsAndMetadata(input) + const accountIds = Object.keys(accountIdsAndMetadata) - if (!wallet || isLedger(wallet)) return + Object.assign(accountMetadataByAccountId, accountIdsAndMetadata) - const walletId = await wallet.getDeviceID() + const { getAccount } = portfolioApi.endpoints - 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 - }) - }), - ) + const accountNumberAccountIdsByChainId = ( + _accountIds: AccountId[], + ): Record => { + return _accountIds.reduce( + (acc, _accountId) => { + const { chainId } = fromAccountId(_accountId) - const accountMetadataByAccountId: AccountMetadataById = {} - const isMultiAccountWallet = wallet.supportsBip44Accounts() - const isMetaMaskMultichainWallet = wallet instanceof MetaMaskShapeShiftMultiChainHDWallet - 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)) - } + if (!acc[chainId]) { + acc[chainId] = [] } - }) + acc[chainId].push(_accountId) - return results + 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)) + } + } }) - await Promise.allSettled(accountNumberAccountIdsPromises) + return results + }) - chainIds = chainIdsWithActivity - } - } finally { - dispatch(portfolio.actions.setIsAccountMetadataLoading(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)), - ), - ) + 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, diff --git a/src/pages/RFOX/components/Stake/StakeInput.tsx b/src/pages/RFOX/components/Stake/StakeInput.tsx index ce1b6b69252..9a80f6b03ba 100644 --- a/src/pages/RFOX/components/Stake/StakeInput.tsx +++ b/src/pages/RFOX/components/Stake/StakeInput.tsx @@ -35,7 +35,7 @@ import { marketApi } from 'state/slices/marketDataSlice/marketDataSlice' import { selectAssetById, selectFeeAssetByChainId, - selectIsAccountMetadataLoading, + selectIsAccountsMetadataLoading, selectMarketDataByAssetIdUserCurrency, selectMarketDataByFilter, selectPortfolioCryptoPrecisionBalanceByFilter, @@ -95,7 +95,7 @@ export const StakeInput: React.FC = ({ select: selectRuneAddress, }) - const isAccountMetadataLoading = useAppSelector(selectIsAccountMetadataLoading) + const isAccountsMetadataLoading = useAppSelector(selectIsAccountsMetadataLoading) const isBridgeRequired = stakingAssetId !== selectedAssetId const dispatch = useAppDispatch() const translate = useTranslate() @@ -434,7 +434,7 @@ export const StakeInput: React.FC = ({ }, [isChainSupportedByWallet, translate]) const submitButtonText = useMemo(() => { - if (isAccountMetadataLoading) return translate('common.accountsLoading') + if (isAccountsMetadataLoading) return translate('common.accountsLoading') return ( errors.amountFieldInput?.message || @@ -447,7 +447,7 @@ export const StakeInput: React.FC = ({ errors.amountFieldInput, errors.manualRuneAddress, translate, - isAccountMetadataLoading, + isAccountsMetadataLoading, ]) if (!selectedAsset) return null @@ -462,7 +462,7 @@ export const StakeInput: React.FC = ({ ) - if (!stakingAssetAccountAddress && !isAccountMetadataLoading) + if (!stakingAssetAccountAddress && !isAccountsMetadataLoading) return ( {headerComponent} @@ -563,13 +563,13 @@ export const StakeInput: React.FC = ({ borderBottomRadius='xl' > = ({ isLoading={isGetApprovalFeesLoading || isStakeFeesLoading} colorScheme={ Boolean(errors.amountFieldInput || errors.manualRuneAddress) && - !isAccountMetadataLoading + !isAccountsMetadataLoading ? 'red' : 'blue' } diff --git a/src/state/slices/portfolioSlice/__snapshots__/portfolioSlice.test.ts.snap b/src/state/slices/portfolioSlice/__snapshots__/portfolioSlice.test.ts.snap index 431f42c313c..6761414e2b6 100644 --- a/src/state/slices/portfolioSlice/__snapshots__/portfolioSlice.test.ts.snap +++ b/src/state/slices/portfolioSlice/__snapshots__/portfolioSlice.test.ts.snap @@ -30,7 +30,8 @@ exports[`portfolioSlice > reducers > upsertPortfolio > Bitcoin > should update s ], }, "enabledAccountIds": {}, - "isAccountMetadataLoading": false, + "isAccountMetadataLoadingByAccountId": {}, + "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -79,7 +80,8 @@ exports[`portfolioSlice > reducers > upsertPortfolio > Bitcoin > should update s ], }, "enabledAccountIds": {}, - "isAccountMetadataLoading": false, + "isAccountMetadataLoadingByAccountId": {}, + "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -151,7 +153,8 @@ exports[`portfolioSlice > reducers > upsertPortfolio > Ethereum and bitcoin > sh ], }, "enabledAccountIds": {}, - "isAccountMetadataLoading": false, + "isAccountMetadataLoadingByAccountId": {}, + "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -208,7 +211,8 @@ exports[`portfolioSlice > reducers > upsertPortfolio > Ethereum and bitcoin > sh ], }, "enabledAccountIds": {}, - "isAccountMetadataLoading": false, + "isAccountMetadataLoadingByAccountId": {}, + "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -248,7 +252,8 @@ exports[`portfolioSlice > reducers > upsertPortfolio > ethereum > should update ], }, "enabledAccountIds": {}, - "isAccountMetadataLoading": false, + "isAccountMetadataLoadingByAccountId": {}, + "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], @@ -301,7 +306,8 @@ exports[`portfolioSlice > reducers > upsertPortfolio > ethereum > should update ], }, "enabledAccountIds": {}, - "isAccountMetadataLoading": false, + "isAccountMetadataLoadingByAccountId": {}, + "isAccountsMetadataLoading": false, "wallet": { "byId": {}, "ids": [], diff --git a/src/state/slices/portfolioSlice/portfolioSlice.ts b/src/state/slices/portfolioSlice/portfolioSlice.ts index 7ddd73a22c2..1a9a24ee69a 100644 --- a/src/state/slices/portfolioSlice/portfolioSlice.ts +++ b/src/state/slices/portfolioSlice/portfolioSlice.ts @@ -40,8 +40,14 @@ export const portfolio = createSlice({ clear: () => { return initialState }, - setIsAccountMetadataLoading: (state, { payload }: { payload: boolean }) => { - state.isAccountMetadataLoading = payload + setIsAccountsMetadataLoading: (state, { payload }: { payload: boolean }) => { + state.isAccountsMetadataLoading = payload + }, + setIsAccountMetadataLoading: ( + state, + { payload }: { payload: { accountId: AccountId; isLoading: boolean } }, + ) => { + state.isAccountMetadataLoadingByAccountId[payload.accountId] = payload.isLoading }, setWalletMeta: ( state, @@ -181,6 +187,7 @@ export const portfolioApi = createApi({ endpoints: build => ({ getAccount: build.query({ queryFn: async ({ accountId, upsertOnFetch }, { dispatch, getState }) => { + dispatch(portfolio.actions.setIsAccountMetadataLoading({ accountId, isLoading: true })) if (!accountId) return { data: cloneDeep(initialState) } const state: ReduxState = getState() as any const assetIds = state.assets.ids @@ -326,6 +333,7 @@ export const portfolioApi = createApi({ })() upsertOnFetch && dispatch(portfolio.actions.upsertPortfolio(data)) + dispatch(portfolio.actions.setIsAccountMetadataLoading({ accountId, isLoading: false })) return { data } } catch (e) { console.error(e) @@ -333,6 +341,7 @@ export const portfolioApi = createApi({ data.accounts.ids.push(accountId) data.accounts.byId[accountId] = { assetIds: [], hasActivity: false } dispatch(portfolio.actions.upsertPortfolio(data)) + dispatch(portfolio.actions.setIsAccountMetadataLoading({ accountId, isLoading: false })) return { data } } }, diff --git a/src/state/slices/portfolioSlice/portfolioSliceCommon.ts b/src/state/slices/portfolioSlice/portfolioSliceCommon.ts index 0385fede352..d3a2be600a1 100644 --- a/src/state/slices/portfolioSlice/portfolioSliceCommon.ts +++ b/src/state/slices/portfolioSlice/portfolioSliceCommon.ts @@ -54,7 +54,8 @@ export type ConnectWallet = { } export type Portfolio = { - isAccountMetadataLoading: boolean + isAccountsMetadataLoading: boolean + isAccountMetadataLoadingByAccountId: Record /** * lookup of accountId -> accountMetadata */ @@ -74,7 +75,8 @@ export type Portfolio = { } export const initialState: Portfolio = { - isAccountMetadataLoading: false, + isAccountsMetadataLoading: false, + isAccountMetadataLoadingByAccountId: {}, accounts: { byId: {}, ids: [], diff --git a/src/state/slices/portfolioSlice/selectors.ts b/src/state/slices/portfolioSlice/selectors.ts index 593f84be7eb..70f5a3f9fe5 100644 --- a/src/state/slices/portfolioSlice/selectors.ts +++ b/src/state/slices/portfolioSlice/selectors.ts @@ -1155,5 +1155,25 @@ export const selectWalletConnectedChainIdsSorted = createDeepEqualOutputSelector }, ) -export const selectIsAccountMetadataLoading = (state: ReduxState) => - state.portfolio.isAccountMetadataLoading +export const selectIsAccountsMetadataLoading = (state: ReduxState) => + state.portfolio.isAccountsMetadataLoading +export const selectIsAccountMetadataLoadingByAccountId = (state: ReduxState) => + state.portfolio.isAccountMetadataLoadingByAccountId +export const selectIsAnyAccountMetadataLoadingForChainId = createSelector( + selectIsAccountMetadataLoadingByAccountId, + selectChainIdParamFromFilter, + (isAccountMetadataLoadingByAccountId, chainId): boolean => { + return Object.entries(isAccountMetadataLoadingByAccountId).some( + ([accountId, isLoading]) => fromAccountId(accountId).chainId === chainId && isLoading, + ) + }, +) +export const selectIsAnyAccountMetadataLoadedForChainId = createSelector( + selectIsAccountMetadataLoadingByAccountId, + selectChainIdParamFromFilter, + (isAccountMetadataLoadingByAccountId, chainId): boolean => { + return Object.entries(isAccountMetadataLoadingByAccountId).some( + ([accountId, isLoading]) => fromAccountId(accountId).chainId === chainId && !isLoading, + ) + }, +) diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 100f8b98911..e4f87fedcd1 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -46,7 +46,8 @@ export const mockStore: ReduxState = { version: 0, rehydrated: false, }, - isAccountMetadataLoading: false, + isAccountsMetadataLoading: false, + isAccountMetadataLoadingByAccountId: {}, accounts: { byId: {}, ids: [],