Skip to content

Commit

Permalink
feat: query transactions on networks independently (#5528)
Browse files Browse the repository at this point in the history
### Description

Alternative implementation of #5432 which addresses the comments raised
in #5432 (comment)
passing state and setState to the `queryTransactionsFeed` function.

### Test plan

- [x] Tested locally on iOS
- [x] Tested locally on Android
- [x] Unit tests updated

### Related issues

- Fixes ACT-1186

### Backwards compatibility

Yes

### Network scalability

If a new NetworkId and/or Network are added in the future, the changes
in this PR will:

- [x] Continue to work without code changes, OR trigger a compilation
error (guaranteeing we find it when a new network is added)
  • Loading branch information
MuckT authored Jun 18, 2024
1 parent cd73184 commit f5da2c9
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 75 deletions.
2 changes: 1 addition & 1 deletion src/transactions/feed/TransactionFeed.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ describe('TransactionFeed', () => {
})

it('renders correct status for a complete transaction', async () => {
mockFetch.mockResponse(JSON.stringify(MOCK_RESPONSE))
mockFetch.mockResponse(JSON.stringify(MOCK_RESPONSE_NO_NEXT_PAGE))

const { getByTestId, getByText } = renderScreen({})

Expand Down
184 changes: 110 additions & 74 deletions src/transactions/feed/queryHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export interface QueryResponse {
}
}

type ActiveRequests = { [key in NetworkId]: boolean }

const TAG = 'transactions/feed/queryHelper'

// Query poll interval
Expand Down Expand Up @@ -79,6 +81,22 @@ export function useFetchTransactions(): QueryHookResult {
// on the home feed, since they get cached in Redux -- this is just a network optimization.
const allowedNetworkIds = getAllowedNetworkIds()

// Track which networks are currently fetching transactions via polling to avoid duplicate requests
const [activePollingRequests, setActivePollingRequests] = useState<ActiveRequests>(
allowedNetworkIds.reduce((acc, networkId) => {
acc[networkId] = false
return acc
}, {} as ActiveRequests)
)

// Track which networks are currently fetching transactions via pagination to avoid duplicate requests
const [activePaginationRequests, setActivePaginationRequests] = useState<ActiveRequests>(
allowedNetworkIds.reduce((acc, networkId) => {
acc[networkId] = false
return acc
}, {} as ActiveRequests)
)

// Track cumulative transactions and most recent page info for all chains in one
// piece of state so that they don't become out of sync.
const [fetchedResult, setFetchedResult] = useState<{
Expand Down Expand Up @@ -107,76 +125,65 @@ export function useFetchTransactions(): QueryHookResult {
const [counter, setCounter] = useState(0)
useInterval(() => setCounter((n) => n + 1), POLL_INTERVAL)

const handleResult = (results: { [key in NetworkId]?: QueryResponse }, isPollResult: boolean) => {
Logger.info(TAG, `Fetched ${isPollResult ? 'new' : 'next page of'} transactions`)

for (const [networkId, result] of Object.entries(results) as Array<
[NetworkId, QueryResponse]
>) {
const returnedTransactions = result.data?.tokenTransactionsV3?.transactions ?? []

const returnedPageInfo = result.data?.tokenTransactionsV3?.pageInfo ?? null

// the initial feed fetch is from polling, exclude polled updates from that scenario
const isPolledUpdate = isPollResult && fetchedResult.pageInfo[networkId] !== null

if (returnedTransactions.length || returnedPageInfo?.hasNextPage) {
setFetchedResult((prev) => ({
transactions: deduplicateTransactions(prev.transactions, returnedTransactions),
// avoid updating pageInfo and hasReturnedTransactions for polled
// updates, as these variables are used for fetching the next pages
pageInfo: isPolledUpdate
? prev.pageInfo
: { ...prev.pageInfo, [networkId]: returnedPageInfo },
hasTransactionsOnCurrentPage: isPolledUpdate
? prev.hasTransactionsOnCurrentPage
: {
...prev.hasTransactionsOnCurrentPage,
[networkId]: returnedTransactions.length > 0,
},
}))

if (isPollResult && returnedTransactions.length) {
// We store the first page in redux to show them to the users when they open the app.
// Filter out now empty transactions to avoid redux issues
const nonEmptyTransactions = returnedTransactions.filter(
(returnedTransaction) => !isEmpty(returnedTransaction)
)
const knownTransactionHashes = transactionHashesByNetwork[networkId]
let hasNewTransaction = false

// Compare the new tx hashes with the ones we already have in redux
for (const tx of nonEmptyTransactions) {
if (!knownTransactionHashes || !knownTransactionHashes.has(tx.transactionHash)) {
hasNewTransaction = true
break // We only need one new tx justify a refresh
}
}
// If there are new transactions update transactions in redux and fetch balances
if (hasNewTransaction) {
dispatch(updateTransactions(networkId, nonEmptyTransactions))
vibrateSuccess()
}
}
}
}
}

const handleError = (error: Error) => {
Logger.error(TAG, 'Error while fetching transactions', error)
}

// Query for new transaction every POLL_INTERVAL
const { loading, error } = useAsync(
async () => {
const result = await queryTransactionsFeed({
await queryTransactionsFeed({
address,
localCurrencyCode,
params: allowedNetworkIds.map((networkId) => {
return { networkId }
}),
onNetworkResponse: (networkId, result) => {
const returnedTransactions = result?.data.tokenTransactionsV3?.transactions ?? []
const returnedPageInfo = result?.data.tokenTransactionsV3?.pageInfo ?? null

// During the initial feed fetch we need to perform some first time setup
const isInitialFetch = fetchedResult.pageInfo[networkId] === null
if (returnedTransactions.length || returnedPageInfo?.hasNextPage) {
setFetchedResult((prev) => ({
transactions: deduplicateTransactions(prev.transactions, returnedTransactions),
pageInfo: isInitialFetch
? { ...prev.pageInfo, [networkId]: returnedPageInfo }
: prev.pageInfo,
hasTransactionsOnCurrentPage: isInitialFetch
? {
...prev.hasTransactionsOnCurrentPage,
[networkId]: returnedTransactions.length > 0,
}
: prev.hasTransactionsOnCurrentPage,
}))
}
if (returnedTransactions.length) {
// We store the first page in redux to show them to the users when they open the app.
// Filter out now empty transactions to avoid redux issues
const nonEmptyTransactions = returnedTransactions.filter(
(returnedTransaction) => !isEmpty(returnedTransaction)
)
const knownTransactionHashes = transactionHashesByNetwork[networkId]
let hasNewTransaction = false

// Compare the new tx hashes with the ones we already have in redux
for (const tx of nonEmptyTransactions) {
if (!knownTransactionHashes || !knownTransactionHashes.has(tx.transactionHash)) {
hasNewTransaction = true
break // We only need one new tx justify a refresh
}
}
// If there are new transactions update transactions in redux and fetch balances
if (hasNewTransaction) {
dispatch(updateTransactions(networkId, nonEmptyTransactions))
vibrateSuccess()
}
}
},
setActiveRequests: setActivePollingRequests,
activeRequests: activePollingRequests,
})
handleResult(result, true)
},
[counter],
{
Expand All @@ -187,7 +194,7 @@ export function useFetchTransactions(): QueryHookResult {
// Query for more transactions if requested
useAsync(
async () => {
if (!fetchingMoreTransactions || !anyNetworkHasMorePages(fetchedResult.pageInfo)) {
if (!anyNetworkHasMorePages(fetchedResult.pageInfo)) {
setFetchingMoreTransactions(false)
return
}
Expand All @@ -201,13 +208,28 @@ export function useFetchTransactions(): QueryHookResult {
return { networkId, afterCursor: pageInfo?.endCursor }
})
.filter((networkParams) => fetchedResult.pageInfo[networkParams.networkId]?.hasNextPage)
const result = await queryTransactionsFeed({
await queryTransactionsFeed({
address,
localCurrencyCode,
params,
onNetworkResponse: (networkId, result) => {
const returnedTransactions = result?.data.tokenTransactionsV3?.transactions ?? []
const returnedPageInfo = result?.data.tokenTransactionsV3?.pageInfo ?? null
if (returnedTransactions.length || returnedPageInfo?.hasNextPage) {
setFetchedResult((prev) => ({
transactions: deduplicateTransactions(prev.transactions, returnedTransactions),
pageInfo: { ...prev.pageInfo, [networkId]: returnedPageInfo },
hasTransactionsOnCurrentPage: {
...prev.hasTransactionsOnCurrentPage,
[networkId]: returnedTransactions.length > 0,
},
}))
}
},
setActiveRequests: setActivePaginationRequests,
activeRequests: activePaginationRequests,
})
setFetchingMoreTransactions(false)
handleResult(result, false)
},
[fetchingMoreTransactions],
{
Expand All @@ -231,10 +253,10 @@ export function useFetchTransactions(): QueryHookResult {
//
// This has the effect of setting the fetchingMoreTransactions flag to true iff
// - We are not already loading
// - There exists at least one chain that has futher pages
// - There exists at least one chain that has further pages
// - EITHER we do not yet have enough TXs, OR NO chains whatsoever produced results
// in the most recent round of fetching (which corresponds to case 2. above
// occuring for all chains simaltaneously)
// occurring for all chains simultaneously)
const { transactions, pageInfo, hasTransactionsOnCurrentPage } = fetchedResult
if (
!loading &&
Expand Down Expand Up @@ -282,31 +304,45 @@ async function queryTransactionsFeed({
address,
localCurrencyCode,
params,
onNetworkResponse,
setActiveRequests,
activeRequests,
}: {
address: string | null
localCurrencyCode: string
params: Array<{
networkId: NetworkId
afterCursor?: string
}>
}): Promise<{ [key in NetworkId]?: QueryResponse }> {
const results = await Promise.all(
params.map(({ networkId, afterCursor }) =>
queryChainTransactionsFeed({
onNetworkResponse: (networkId: NetworkId, data: QueryResponse | null) => void
setActiveRequests: (updateFunc: (prevState: ActiveRequests) => ActiveRequests) => void
activeRequests: ActiveRequests
}): Promise<void> {
// Launch all network requests without waiting for each to finish before starting the next
const requests = params.map(async ({ networkId, afterCursor }) => {
// Prevent duplicate requests for the same network
if (activeRequests[networkId]) {
Logger.info(TAG, `Skipping fetch for ${networkId} as it is already active`)
return
} else {
Logger.info(TAG, `Fetching transactions for ${networkId} with cursor: ${afterCursor}`)
setActiveRequests((prev) => ({ ...prev, [networkId]: true }))
}
try {
const result = await queryChainTransactionsFeed({
address,
localCurrencyCode,
networkId,
afterCursor,
})
)
)

return results.reduce((acc, result, index) => {
return {
...acc,
[params[index].networkId]: result,
Logger.info(TAG, `Fetched transactions for ${networkId}`, result)
onNetworkResponse(networkId, result) // Update state as soon as data is available
} finally {
setActiveRequests((prev) => ({ ...prev, [networkId]: false }))
}
}, {})
})

await Promise.all(requests) // Wait for all requests to finish for use in useAsync hooks
}

async function queryChainTransactionsFeed({
Expand Down

0 comments on commit f5da2c9

Please sign in to comment.