Skip to content

Commit

Permalink
fix: only fetch transaction history once on load (#5673)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaladinlight authored Nov 22, 2023
1 parent 61c87f3 commit ba2fa07
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 107 deletions.
4 changes: 2 additions & 2 deletions src/components/Layout/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { useWallet } from 'hooks/useWallet/useWallet'
import { portfolio } from 'state/slices/portfolioSlice/portfolioSlice'
import { isUtxoAccountId } from 'state/slices/portfolioSlice/utils'
import {
selectPortfolioLoadingStatus,
selectPortfolioDegradedState,
selectShowSnapsModal,
selectWalletAccountIds,
selectWalletId,
Expand Down Expand Up @@ -59,7 +59,7 @@ const hamburgerIcon = <HamburgerIcon />

export const Header = memo(() => {
const { onToggle, isOpen, onClose } = useDisclosure()
const isDegradedState = useSelector(selectPortfolioLoadingStatus) === 'error'
const isDegradedState = useSelector(selectPortfolioDegradedState)
const snapModal = useModal('snaps')
const isSnapInstalled = useIsSnapInstalled()
const previousSnapInstall = usePrevious(isSnapInstalled)
Expand Down
84 changes: 34 additions & 50 deletions src/context/AppProvider/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useToast } from '@chakra-ui/react'
import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip'
import type { AssetId } from '@shapeshiftoss/caip'
import {
avalancheChainId,
bchChainId,
Expand All @@ -13,7 +13,6 @@ import {
} from '@shapeshiftoss/caip'
import { DEFAULT_HISTORY_TIMEFRAME } from 'constants/Config'
import difference from 'lodash/difference'
import pull from 'lodash/pull'
import React, { useEffect } from 'react'
import { useTranslate } from 'react-polyglot'
import { useSelector } from 'react-redux'
Expand All @@ -25,8 +24,6 @@ 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 type { BN } from 'lib/bignumber/bignumber'
import { bnOrZero } from 'lib/bignumber/bignumber'
import { setTimeoutAsync } from 'lib/utils'
import { nftApi } from 'state/apis/nft/nftApi'
import { snapshotApi } from 'state/apis/snapshot/snapshot'
Expand All @@ -45,6 +42,7 @@ import {
} from 'state/slices/opportunitiesSlice/thunks'
import { DefiProvider, DefiType } from 'state/slices/opportunitiesSlice/types'
import { portfolio, portfolioApi } from 'state/slices/portfolioSlice/portfolioSlice'
import type { AccountMetadataById } from 'state/slices/portfolioSlice/portfolioSliceCommon'
import { preferences } from 'state/slices/preferencesSlice/preferencesSlice'
import {
selectAssetIds,
Expand Down Expand Up @@ -105,58 +103,53 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
useEffect(() => {
if (!wallet) return
;(async () => {
const chainIds = Array.from(supportedChains).filter(chainId =>
let chainIds = Array.from(supportedChains).filter(chainId =>
walletSupportsChain({ chainId, wallet, isSnapInstalled }),
)

const accountMetadataByAccountId: AccountMetadataById = {}
const isMultiAccountWallet = wallet.supportsBip44Accounts()
for (let accountNumber = 0; chainIds.length > 0; accountNumber++) {
// only some wallets support multi account
if (accountNumber > 0 && !isMultiAccountWallet) break

const input = { accountNumber, chainIds, wallet }
const accountMetadataByAccountId = await deriveAccountIdsAndMetadata(input)
const accountIds: AccountId[] = Object.keys(accountMetadataByAccountId)
const accountIdsAndMetadata = await deriveAccountIdsAndMetadata(input)
const accountIds = Object.keys(accountIdsAndMetadata)

Object.assign(accountMetadataByAccountId, accountIdsAndMetadata)

const { getAccount } = portfolioApi.endpoints
const opts = { forceRefetch: true }
// do *not* upsertOnFetch here - we need to check if the fetched account is empty
const accountPromises = accountIds.map(accountId =>
dispatch(getAccount.initiate({ accountId }, opts)),
dispatch(getAccount.initiate({ accountId }, { forceRefetch: true })),
)

const accountResults = await Promise.allSettled(accountPromises)
/**
* because UTXO chains can have multiple accounts per number, we need to aggregate
* balance by chain id to see if we fetch the next by accountNumber
*/
const balanceByChainId = accountResults.reduce<Record<ChainId, BN>>((acc, res, idx) => {
if (res.status === 'rejected') return acc

let chainIdsWithActivity: string[] = []
accountResults.forEach((res, idx) => {
if (res.status === 'rejected') return

const { data: account } = res.value
if (!account) return acc
if (!account) return

const accountId = accountIds[idx]
const { chainId } = fromAccountId(accountId)
const accountBalance = Object.values(account.accountBalances.byId).reduce<BN>(
(acc, byAssetId) => {
Object.values(byAssetId).forEach(balance => (acc = acc.plus(bnOrZero(balance))))
return acc
},
bnOrZero(0),
)
acc[chainId] = bnOrZero(acc[chainId]).plus(accountBalance)
// don't upsert empty accounts past account 0
if (accountNumber > 0 && accountBalance.eq(0)) return acc
const accountMetadata = accountMetadataByAccountId[accountId]
const payload = { [accountId]: accountMetadata }
dispatch(portfolio.actions.upsertAccountMetadata(payload))
const { hasActivity } = account.accounts.byId[accountId]

// don't add accounts with no activity past account 0
if (accountNumber > 0 && !hasActivity) return delete accountMetadataByAccountId[accountId]

// unique set to handle utxo chains with multiple account types per account
chainIdsWithActivity = Array.from(new Set([...chainIdsWithActivity, chainId]))

dispatch(portfolio.actions.upsertPortfolio(account))
return acc
}, {})

/**
* if the balance for all accounts for the current chainId and accountNumber
* is zero, we've exhausted that chain, don't fetch more of them
*/
Object.entries(balanceByChainId).forEach(([chainId, balance]) => {
if (balance.eq(0)) pull(chainIds, chainId) // pull mutates chainIds, but we want to
})

chainIds = chainIdsWithActivity
}

dispatch(portfolio.actions.upsertAccountMetadata(accountMetadataByAccountId))
})()
}, [dispatch, wallet, supportedChains, isSnapInstalled])

Expand All @@ -169,21 +162,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
// once portfolio is done loading, fetch all transaction history
useEffect(() => {
;(async () => {
if (!requestedAccountIds.length) return
if (portfolioLoadingStatus === 'loading') return

const { getAllTxHistory } = txHistoryApi.endpoints

try {
await dispatch(getAllTxHistory.initiate(requestedAccountIds))
} finally {
// add any nft assets detected in the tx history state.
// this will ensure we have all nft assets that have been associated with the account in the assetSlice with parsed metadata.
// additional nft asset upserts will be handled by the transactions websocket subscription.
// NOTE: We currently upsert NFTs in nftApi, which blockbook data currently overwrites, however, said blockbook data is borked
// TODO: remove me or uncomment me when blockbook data is fixed
// const txsById = store.getState().txHistory.txs.byId
// dispatch(assetsSlice.actions.upsertAssets(makeNftAssetsFromTxs(Object.values(txsById))))
}
await dispatch(getAllTxHistory.initiate(requestedAccountIds))
})()
}, [dispatch, requestedAccountIds, portfolioLoadingStatus])

Expand Down
4 changes: 0 additions & 4 deletions src/context/TransactionsProvider/TransactionsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,6 @@ export const TransactionsProvider: React.FC<TransactionsProviderProps> = ({ chil
if (!wallet) return
const accountIds = Object.keys(portfolioAccountMetadata)
if (!accountIds.length) return
// this looks useless, but prevents attempting to subscribe multiple times
// something further up the tree from this provider is causing renders when the portfolio status changes,
// even though it shouldn't
if (portfolioLoadingStatus === 'loading') return
;(() => {
accountIds.forEach(accountId => {
const { chainId } = fromAccountId(accountId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Object {
"assetIds": Array [
"bip122:000000000019d6689c085ae165831e93/slip44:0",
],
"hasActivity": true,
},
},
"ids": Array [
Expand Down Expand Up @@ -61,11 +62,13 @@ Object {
"assetIds": Array [
"bip122:000000000019d6689c085ae165831e93/slip44:0",
],
"hasActivity": true,
},
"bip122:000000000019d6689c085ae165831e93:zpub6qk8s2NQsYG6X2Mm6iU2ii3yTAqDb2XqnMu9vo2WjvqwjSvjjiYQQveYXbPxrnRT5Yb5p0x934be745172066EDF795ffc5EA9F28f19b440c637BaBw1wowPwbS8fj7uCfj3UhqhD2LLbvY6Ni1w": Object {
"assetIds": Array [
"bip122:000000000019d6689c085ae165831e93/slip44:0",
],
"hasActivity": true,
},
},
"ids": Array [
Expand Down Expand Up @@ -116,6 +119,7 @@ Object {
"assetIds": Array [
"bip122:000000000019d6689c085ae165831e93/slip44:0",
],
"hasActivity": true,
},
"eip155:1:0x9a2d593725045d1727d525dd07a396f9ff079bb1": Object {
"assetIds": Array [
Expand All @@ -124,6 +128,7 @@ Object {
"eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"eip155:1/erc20:0x5f18c75abdae578b483e5f43f12a39cf75b973a9",
],
"hasActivity": true,
},
"eip155:1:0xea674fdde714fd979de3edf0f56aa9716b898ec8": Object {
"assetIds": Array [
Expand All @@ -132,6 +137,7 @@ Object {
"eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"eip155:1/erc20:0x5f18c75abdae578b483e5f43f12a39cf75b973a9",
],
"hasActivity": true,
},
},
"ids": Array [
Expand Down Expand Up @@ -177,6 +183,7 @@ Object {
"assetIds": Array [
"bip122:000000000019d6689c085ae165831e93/slip44:0",
],
"hasActivity": true,
},
"eip155:1:0x6c8a778ef52e121b7dff1154c553662306a970e9": Object {
"assetIds": Array [
Expand All @@ -186,6 +193,7 @@ Object {
"eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d",
"eip155:1/erc20:0xf0939011a9bb95c3b791f0cb546377ed2693a574",
],
"hasActivity": true,
},
},
"ids": Array [
Expand Down Expand Up @@ -224,6 +232,7 @@ Object {
"eip155:1/slip44:60",
"eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d",
],
"hasActivity": true,
},
},
"ids": Array [
Expand Down Expand Up @@ -266,12 +275,14 @@ Object {
"eip155:1/slip44:60",
"eip155:1/erc20:0xc770eefad204b5180df6a14ee197d99d808ee52d",
],
"hasActivity": true,
},
"eip155:1:0xea674fdde714fd979de3edf0f56aa9716b898ec8": Object {
"assetIds": Array [
"eip155:1/slip44:60",
"eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
],
"hasActivity": true,
},
},
"ids": Array [
Expand Down
2 changes: 1 addition & 1 deletion src/state/slices/portfolioSlice/portfolioSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const portfolioApi = createApi({
console.error(e)
const data = cloneDeep(initialState)
data.accounts.ids.push(accountId)
data.accounts.byId[accountId] = { assetIds: [] }
data.accounts.byId[accountId] = { assetIds: [], hasActivity: false }
dispatch(portfolio.actions.upsertPortfolio(data))
return { data }
}
Expand Down
1 change: 1 addition & 0 deletions src/state/slices/portfolioSlice/portfolioSliceCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Nominal } from 'types/common'
export type PortfolioAccount = {
/** The asset ids belonging to an account */
assetIds: AssetId[]
hasActivity?: boolean
}

export type PortfolioAccounts = {
Expand Down
9 changes: 8 additions & 1 deletion src/state/slices/portfolioSlice/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,19 @@ export const selectPortfolioLoadingStatus = createSelector(
selectPortfolioLoadingStatusGranular,
(portfolioLoadingStatusGranular): PortfolioLoadingStatus => {
const vals = values(portfolioLoadingStatusGranular)
if (vals.every(val => val === 'loading')) return 'loading'
if (vals.some(val => val === 'loading')) return 'loading'
if (vals.some(val => val === 'error')) return 'error'
return 'success'
},
)

export const selectPortfolioDegradedState = createSelector(
selectPortfolioLoadingStatusGranular,
(portfolioLoadingStatusGranular): boolean => {
return values(portfolioLoadingStatusGranular).some(val => val === 'error')
},
)

export const selectPortfolioTotalUserCurrencyBalance = createSelector(
selectPortfolioUserCurrencyBalances,
(portfolioUserCurrencyBalances): string =>
Expand Down
Loading

0 comments on commit ba2fa07

Please sign in to comment.