From 20f57fc30837941d2f801ce2109a4dd79263aa35 Mon Sep 17 00:00:00 2001 From: woody <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 7 May 2024 11:12:28 +1000 Subject: [PATCH] feat: account management wiring progression (#6798) * feat: initial utxo multi account rows for import accounts * fix: load native asset balances in import accounts * styles * fix: use updated query key for loading state * chore: actioned apojuice code review feedback * chore: actioned gome code review feedback --------- Co-authored-by: reallybeard <89934888+reallybeard@users.noreply.github.com> --- .../components/ImportAccounts.tsx | 172 +++++++++++------- .../components/SelectChain.tsx | 71 +++----- .../ManageAccounts/ManageAccountsModal.tsx | 23 +-- .../queries/accountManagement.ts | 42 +++-- src/state/slices/portfolioSlice/selectors.ts | 9 + 5 files changed, 174 insertions(+), 143 deletions(-) diff --git a/src/components/ManageAccountsDrawer/components/ImportAccounts.tsx b/src/components/ManageAccountsDrawer/components/ImportAccounts.tsx index 2b866ee4ce3..33922d6fdf8 100644 --- a/src/components/ManageAccountsDrawer/components/ImportAccounts.tsx +++ b/src/components/ManageAccountsDrawer/components/ImportAccounts.tsx @@ -23,14 +23,14 @@ import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' import { RawText } from 'components/Text' import { useToggle } from 'hooks/useToggle/useToggle' import { useWallet } from 'hooks/useWallet/useWallet' +import { fromBaseUnit } from 'lib/math' import { isUtxoAccountId } from 'lib/utils/utxo' import { portfolio, portfolioApi } from 'state/slices/portfolioSlice/portfolioSlice' import { accountIdToLabel } from 'state/slices/portfolioSlice/utils' import { selectFeeAssetByChainId, selectHighestAccountNumberForChainId, - selectIsAccountIdEnabled, - selectPortfolioCryptoPrecisionBalanceByFilter, + selectIsAnyAccountIdEnabled, } from 'state/slices/selectors' import { useAppDispatch, useAppSelector } from 'state/store' @@ -46,61 +46,88 @@ export type ImportAccountsProps = { } type TableRowProps = { - accountId: AccountId + accountIds: AccountId[] accountNumber: number asset: Asset - onAccountIdActiveChange: (accountId: AccountId, isActive: boolean) => void + onActiveAccountIdsChange: (accountIds: AccountId[], isActive: boolean) => void +} + +type TableRowAccountProps = { + accountId: AccountId + asset: Asset } const disabledProps = { opacity: 0.5, cursor: 'not-allowed', userSelect: 'none' } +const TableRowAccount = forwardRef(({ asset, accountId }, ref) => { + const accountLabel = useMemo(() => accountIdToLabel(accountId), [accountId]) + const pubkey = useMemo(() => fromAccountId(accountId).account, [accountId]) + const isUtxoAccount = useMemo(() => isUtxoAccountId(accountId), [accountId]) + + const { data: account, isLoading } = useQuery(accountManagement.getAccount(accountId)) + + const assetBalanceCryptoPrecision = useMemo(() => { + if (!account) return '0' + return fromBaseUnit(account.balance, asset.precision) + }, [account, asset.precision]) + + return ( + <> + + +
+ +
+
+ + + {isLoading ? ( + + ) : ( + + )} + + + ) +}) + const TableRow = forwardRef( - ({ asset, accountId, accountNumber, onAccountIdActiveChange }, ref) => { - const translate = useTranslate() - const accountLabel = useMemo(() => accountIdToLabel(accountId), [accountId]) - const balanceFilter = useMemo(() => ({ assetId: asset.assetId, accountId }), [asset, accountId]) - const isAccountEnabledFilter = useMemo(() => ({ accountId }), [accountId]) + ({ asset, accountNumber, accountIds, onActiveAccountIdsChange }, ref) => { const isAccountEnabledInRedux = useAppSelector(state => - selectIsAccountIdEnabled(state, isAccountEnabledFilter), + selectIsAnyAccountIdEnabled(state, accountIds), ) const [isAccountActive, toggleIsAccountActive] = useToggle(isAccountEnabledInRedux) useEffect(() => { - onAccountIdActiveChange(accountId, isAccountActive) - }, [accountId, isAccountActive, isAccountEnabledInRedux, onAccountIdActiveChange]) - - // TODO: Redux wont have this for new accounts and will be 0, so we'll need to fetch it - const assetBalancePrecision = useAppSelector(s => - selectPortfolioCryptoPrecisionBalanceByFilter(s, balanceFilter), - ) - const pubkey = useMemo(() => fromAccountId(accountId).account, [accountId]) + onActiveAccountIdsChange(accountIds, isAccountActive) + }, [accountIds, isAccountActive, isAccountEnabledInRedux, onActiveAccountIdsChange]) - const isUtxoAccount = useMemo(() => isUtxoAccountId(accountId), [accountId]) + const firstAccount = useMemo(() => accountIds[0], [accountIds]) + const otherAccountIds = useMemo(() => accountIds.slice(1), [accountIds]) + const otherAccounts = useMemo(() => { + return otherAccountIds.map(accountId => ( + + + + + )) + }, [asset, isAccountActive, otherAccountIds, ref]) return ( - - - {accountNumber} - - - - - - -
- {isUtxoAccount ? ( - {`${accountLabel} ${translate('common.account')}`} - ) : ( - - )} -
-
- - - - - + <> + + + + + + {accountNumber} + + + + + {otherAccounts} + ) }, ) @@ -135,10 +162,20 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => { ) const chainNamespaceDisplayName = asset?.networkName ?? '' const [accounts, setAccounts] = useState< - { accountId: AccountId; accountMetadata: AccountMetadata; hasActivity: boolean }[] + { accountId: AccountId; accountMetadata: AccountMetadata; hasActivity: boolean }[][] >([]) const queryClient = useQueryClient() - const isLoading = useIsFetching({ queryKey: ['accountManagement'] }) > 0 + const isLoading = + useIsFetching({ + predicate: query => { + return ( + query.queryKey[0] === 'accountManagement' && + ['accountIdWithActivityAndMetadata', 'firstAccountIdsWithActivityAndMetadata'].some( + str => str === query.queryKey[1], + ) + ) + }, + }) > 0 const [accountIdActiveStateUpdate, setAccountIdActiveStateUpdate] = useState< Record >({}) @@ -162,7 +199,7 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => { const handleLoadMore = useCallback(async () => { if (!wallet) return const accountNumber = accounts.length - const accountResult = await queryClient.fetchQuery( + const accountResults = await queryClient.fetchQuery( reactQueries.accountManagement.accountIdWithActivityAndMetadata( accountNumber, chainId, @@ -170,16 +207,18 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => { walletDeviceId, ), ) - if (!accountResult) return + if (!accountResults.length) return setAccounts(previousAccounts => { - const { accountId, accountMetadata, hasActivity } = accountResult - return [...previousAccounts, { accountId, accountMetadata, hasActivity }] + return [...previousAccounts, accountResults] }) }, [accounts, chainId, queryClient, wallet, walletDeviceId]) - const handleAccountIdActiveChange = useCallback((accountId: AccountId, isActive: boolean) => { + const handleAccountIdsActiveChange = useCallback((accountIds: AccountId[], isActive: boolean) => { setAccountIdActiveStateUpdate(previousState => { - return { ...previousState, [accountId]: isActive } + const stateUpdate = accountIds.reduce((accumulator, accountId) => { + return { ...accumulator, [accountId]: isActive } + }, {}) + return { ...previousState, ...stateUpdate } }) }, []) @@ -191,12 +230,12 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => { await dispatch(portfolioApi.endpoints.getAccount.initiate({ accountId, upsertOnFetch: true })) } - const accountMetadataByAccountId = accounts.reduce( - (accumulator, { accountId, accountMetadata }) => { - return { ...accumulator, [accountId]: accountMetadata } - }, - {}, - ) + const accountMetadataByAccountId = accounts.reduce((accumulator, accounts) => { + const obj = accounts.reduce((innerAccumulator, { accountId, accountMetadata }) => { + return { ...innerAccumulator, [accountId]: accountMetadata } + }, {}) + return { ...accumulator, ...obj } + }, {}) dispatch( portfolio.actions.upsertAccountMetadata({ @@ -216,16 +255,19 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => { const accountRows = useMemo(() => { if (!asset) return null - return accounts.map(({ accountId }, accountNumber) => ( - - )) - }, [accounts, asset, handleAccountIdActiveChange]) + return accounts.map((accountsForAccountNumber, accountNumber) => { + const accountIds = accountsForAccountNumber.map(({ accountId }) => accountId) + return ( + + ) + }) + }, [accounts, asset, handleAccountIdsActiveChange]) if (!asset) { console.error(`No fee asset found for chainId: ${chainId}`) diff --git a/src/components/ManageAccountsDrawer/components/SelectChain.tsx b/src/components/ManageAccountsDrawer/components/SelectChain.tsx index 65afc46f313..69ec1738f5c 100644 --- a/src/components/ManageAccountsDrawer/components/SelectChain.tsx +++ b/src/components/ManageAccountsDrawer/components/SelectChain.tsx @@ -1,19 +1,9 @@ -import { SearchIcon } from '@chakra-ui/icons' -import { - Box, - Button, - Input, - InputGroup, - InputLeftElement, - SimpleGrid, - VStack, -} from '@chakra-ui/react' +import { Button, SimpleGrid, Stack, VStack } from '@chakra-ui/react' import type { ChainId } from '@shapeshiftoss/caip' -import type { FormEvent } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' -import { useForm } from 'react-hook-form' import { useTranslate } from 'react-polyglot' import { LazyLoadAvatar } from 'components/LazyLoadAvatar' +import { GlobalFilter } from 'components/StakingVaults/GlobalFilter' import { RawText } from 'components/Text' import { assertGetChainAdapter, chainIdToFeeAssetId } from 'lib/utils' import { selectAssetById, selectWalletSupportedChainIds } from 'state/slices/selectors' @@ -22,6 +12,8 @@ import { useAppSelector } from 'state/store' import { filterChainIdsBySearchTerm } from '../helpers' import { DrawerContentWrapper } from './DrawerContent' +const inputGroupProps = { size: 'lg' } + export type SelectChainProps = { onSelectChainId: (chainId: ChainId) => void onClose: () => void @@ -57,33 +49,24 @@ const ChainButton = ({ export const SelectChain = ({ onSelectChainId, onClose }: SelectChainProps) => { const translate = useTranslate() const [searchTermChainIds, setSearchTermChainIds] = useState([]) + const [searchQuery, setSearchQuery] = useState('') const walletSupportedChainIds = useAppSelector(selectWalletSupportedChainIds) - const handleSubmit = useCallback((e: FormEvent) => e.preventDefault(), []) - - const { register, watch } = useForm<{ search: string }>({ - mode: 'onChange', - defaultValues: { - search: '', - }, - }) - const searchString = watch('search') - - const searching = useMemo(() => searchString.length > 0, [searchString]) + const isSearching = useMemo(() => searchQuery.length > 0, [searchQuery]) useEffect(() => { - if (!searching) return + if (!isSearching) return - setSearchTermChainIds(filterChainIdsBySearchTerm(searchString, walletSupportedChainIds)) - }, [searchString, searching, walletSupportedChainIds]) + setSearchTermChainIds(filterChainIdsBySearchTerm(searchQuery, walletSupportedChainIds)) + }, [searchQuery, isSearching, walletSupportedChainIds]) const chainButtons = useMemo(() => { - const listChainIds = searching ? searchTermChainIds : walletSupportedChainIds + const listChainIds = isSearching ? searchTermChainIds : walletSupportedChainIds return listChainIds.map(chainId => { return }) - }, [onSelectChainId, searchTermChainIds, searching, walletSupportedChainIds]) + }, [onSelectChainId, searchTermChainIds, isSearching, walletSupportedChainIds]) const footer = useMemo(() => { return ( @@ -97,31 +80,19 @@ export const SelectChain = ({ onSelectChainId, onClose }: SelectChainProps) => { const body = useMemo(() => { return ( - <> - - - {/* Override zIndex to prevent element displaying on overlay components */} - - - - - - - + + + {chainButtons} - + ) - }, [chainButtons, handleSubmit, register, translate]) + }, [chainButtons, searchQuery, translate]) return ( + ) @@ -115,12 +113,14 @@ export const ManageAccountsModal = ({ onClose={handleDrawerClose} chainId={selectedChainId} /> - + - - - {translate(title)} - + + + + {translate(title)} + + {translate('accountManagement.manageAccounts.description')} @@ -146,12 +146,13 @@ export const ManageAccountsModal = ({ colorScheme='blue' onClick={handleClickAddChain} width='full' + size='lg' isDisabled={disableAddChain} _disabled={disabledProp} > {translate('accountManagement.manageAccounts.addChain')} - diff --git a/src/react-queries/queries/accountManagement.ts b/src/react-queries/queries/accountManagement.ts index 79c1b4b0912..13ec8f57b4d 100644 --- a/src/react-queries/queries/accountManagement.ts +++ b/src/react-queries/queries/accountManagement.ts @@ -14,17 +14,29 @@ const getAccountIdsWithActivityAndMetadata = async ( ) => { const input = { accountNumber, chainIds: [chainId], wallet } const accountIdsAndMetadata = await deriveAccountIdsAndMetadata(input) - const [[accountId, accountMetadata]] = Object.entries(accountIdsAndMetadata) - const { account: pubkey } = fromAccountId(accountId) - const adapter = assertGetChainAdapter(chainId) - const account = await adapter.getAccount(pubkey) - const hasActivity = checkAccountHasActivity(account) + return Promise.all( + Object.entries(accountIdsAndMetadata).map(async ([accountId, accountMetadata]) => { + const { account: pubkey } = fromAccountId(accountId) + const adapter = assertGetChainAdapter(chainId) + const account = await adapter.getAccount(pubkey) + const hasActivity = checkAccountHasActivity(account) - return { accountId, accountMetadata, hasActivity } + return { accountId, accountMetadata, hasActivity } + }), + ) } export const accountManagement = createQueryKeys('accountManagement', { + getAccount: (accountId: AccountId) => ({ + queryKey: ['getAccount', accountId], + queryFn: async () => { + const { chainId, account: pubkey } = fromAccountId(accountId) + const adapter = assertGetChainAdapter(chainId) + const account = await adapter.getAccount(pubkey) + return account + }, + }), accountIdWithActivityAndMetadata: ( accountNumber: number, chainId: ChainId, @@ -55,27 +67,23 @@ export const accountManagement = createQueryKeys('accountManagement', { accountId: AccountId accountMetadata: AccountMetadata hasActivity: boolean - }[] = [] + }[][] = [] if (!wallet) return [] while (true) { try { - const accountResult = await getAccountIdsWithActivityAndMetadata( + if (accountNumber >= accountNumberLimit) { + break + } + + const accountResults = await getAccountIdsWithActivityAndMetadata( accountNumber, chainId, wallet, ) - if (!accountResult) break - - const { accountId, accountMetadata, hasActivity } = accountResult - - if (accountNumber >= accountNumberLimit) { - break - } - - accounts.push({ accountId, accountMetadata, hasActivity }) + accounts.push(accountResults) } catch (error) { console.error(error) break diff --git a/src/state/slices/portfolioSlice/selectors.ts b/src/state/slices/portfolioSlice/selectors.ts index b0a1a91a0b2..60d79f64f49 100644 --- a/src/state/slices/portfolioSlice/selectors.ts +++ b/src/state/slices/portfolioSlice/selectors.ts @@ -97,6 +97,15 @@ export const selectIsAccountIdEnabled = createCachedSelector( }, )((_s: ReduxState, filter) => filter?.accountId ?? 'accountId') +export const selectIsAnyAccountIdEnabled = createCachedSelector( + selectPortfolioAccounts, + (_state: ReduxState, accountIds: AccountId[]) => accountIds, + (accountsById, accountIds): boolean => { + if (accountIds.length === 0) return false + return accountIds.some(accountId => accountsById[accountId] !== undefined) + }, +)((_s: ReduxState, accountIds) => JSON.stringify(accountIds)) + export const selectPortfolioAssetIds = createDeepEqualOutputSelector( selectPortfolioAccountBalancesBaseUnit, (accountBalancesById): AssetId[] => {