Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: query transactions on networks independently #5528

Merged
merged 7 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
}
}

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

const TAG = 'transactions/feed/queryHelper'

// Query poll interval
Expand Down Expand Up @@ -79,6 +81,22 @@
// 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 @@
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,

Check warning on line 152 in src/transactions/feed/queryHelper.ts

View check run for this annotation

Codecov / codecov/patch

src/transactions/feed/queryHelper.ts#L152

Added line #L152 was not covered by tests
hasTransactionsOnCurrentPage: isInitialFetch
? {
...prev.hasTransactionsOnCurrentPage,
[networkId]: returnedTransactions.length > 0,
}
: prev.hasTransactionsOnCurrentPage,

Check warning on line 158 in src/transactions/feed/queryHelper.ts

View check run for this annotation

Codecov / codecov/patch

src/transactions/feed/queryHelper.ts#L158

Added line #L158 was not covered by tests
}))
}
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 @@
// 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 @@
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 @@
//
// 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 @@
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

Check warning on line 326 in src/transactions/feed/queryHelper.ts

View check run for this annotation

Codecov / codecov/patch

src/transactions/feed/queryHelper.ts#L325-L326

Added lines #L325 - L326 were not covered by tests
Comment on lines +324 to +326
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we could avoid that check and the need to pass setActiveRequests / activeRequests
Is the problem that useAsync can call queryTransactionsFeed multiple times, but we effectively only care about the last one?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this because some slow networks, such as polygon, take longer than the polling interval to return a result, so we can end up with multiple pending requests that are slow to resolve.

} 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