From 227f838ff58cfe9e106de289c0e1d068af5d4fd1 Mon Sep 17 00:00:00 2001 From: tom Date: Tue, 18 Nov 2025 12:17:42 +0100 Subject: [PATCH] Create `useApiQueries` hook to manage multiple API requests to the same resource in parallel --- lib/api/useApiQueries.ts | 50 +++++++++++++++++ types/api/noves.ts | 5 -- types/api/transaction.ts | 3 -- ui/txs/TxTranslationType.tsx | 14 ++--- ui/txs/TxsContent.tsx | 12 +++-- ui/txs/TxsList.tsx | 30 ++++++----- ui/txs/TxsListItem.tsx | 23 ++++++-- ui/txs/TxsTable.tsx | 31 ++++++----- ui/txs/TxsTableItem.tsx | 23 ++++++-- ui/txs/noves/useDescribeTxs.tsx | 96 +++++++++------------------------ 10 files changed, 163 insertions(+), 124 deletions(-) create mode 100644 lib/api/useApiQueries.ts diff --git a/lib/api/useApiQueries.ts b/lib/api/useApiQueries.ts new file mode 100644 index 0000000000..002fb44ddb --- /dev/null +++ b/lib/api/useApiQueries.ts @@ -0,0 +1,50 @@ +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQueries } from '@tanstack/react-query'; + +import type { ResourceError, ResourceName, ResourcePayload } from './resources'; +import useApiFetch from './useApiFetch'; +import type { Params as ApiQueryParams } from './useApiQuery'; +import { getResourceKey } from './useApiQuery'; + +export interface ReturnType { + isLoading: boolean; + isPlaceholderData: boolean; + isError: boolean; + isSuccess: boolean; + data: Array> | undefined; + error: ResourceError | null | undefined; +} + +export default function useApiQueries( + resource: R, + params: Array>, + queryOptions: Partial, ResourceError, Array>>, 'queries'>>, +) { + const apiFetch = useApiFetch(); + + return useQueries, ResourceError>>, ReturnType>({ + queries: params.map(({ pathParams, queryParams, queryOptions, fetchParams, logError, chain }) => { + return { + queryKey: getResourceKey(resource, { pathParams, queryParams, chainId: chain?.id }), + queryFn: async({ signal }) => { + return apiFetch(resource, { pathParams, queryParams, logError, chain, fetchParams: { ...fetchParams, signal } }) as Promise>; + }, + ...queryOptions, + }; + }), + combine: (results) => { + const isError = results.some((result) => result.isError); + const isSuccess = results.every((result) => result.isSuccess); + + return { + isLoading: results.some((result) => result.isLoading), + isPlaceholderData: results.some((result) => result.isPlaceholderData), + isError, + isSuccess, + data: isSuccess ? results.map((result) => result.data).flat() as Array> : undefined, + error: isError ? results.find((result) => result.error)?.error : undefined, + } satisfies ReturnType; + }, + ...queryOptions, + }); +} diff --git a/types/api/noves.ts b/types/api/noves.ts index edb6e42bdb..f5e1c42e8f 100644 --- a/types/api/noves.ts +++ b/types/api/noves.ts @@ -118,8 +118,3 @@ export type NovesDescribeTxsResponse = { type: string; description: string; }; - -export interface NovesTxTranslation { - data?: NovesDescribeTxsResponse; - isLoading: boolean; -} diff --git a/types/api/transaction.ts b/types/api/transaction.ts index 2b4c44b55e..bbe4d26cbf 100644 --- a/types/api/transaction.ts +++ b/types/api/transaction.ts @@ -4,7 +4,6 @@ import type { BlockTransactionsResponse } from './block'; import type { DecodedInput } from './decodedInput'; import type { Fee } from './fee'; import type { ChainInfo, MessageStatus } from './interop'; -import type { NovesTxTranslation } from './noves'; import type { OptimisticL2WithdrawalClaimInfo, OptimisticL2WithdrawalStatus } from './optimisticL2'; import type { ScrollL2BlockStatus } from './scrollL2'; import type { TokenInfo } from './token'; @@ -104,8 +103,6 @@ export type Transaction = { blob_gas_price?: string; burnt_blob_fee?: string; max_fee_per_blob_gas?: string; - // Noves-fi - translation?: NovesTxTranslation; arbitrum?: ArbitrumTransactionData; scroll?: ScrollTransactionData; // EIP-7702 diff --git a/ui/txs/TxTranslationType.tsx b/ui/txs/TxTranslationType.tsx index 5fa5eb5ad1..af75b1cbe1 100644 --- a/ui/txs/TxTranslationType.tsx +++ b/ui/txs/TxTranslationType.tsx @@ -8,22 +8,22 @@ import { camelCaseToSentence } from './noves/utils'; import TxType from './TxType'; export interface Props { - types: Array; + txTypes: Array; isLoading?: boolean; - translatationType: string | undefined; + type: string | undefined; } -const TxTranslationType = ({ types, isLoading, translatationType }: Props) => { +const FILTERED_TYPES = [ 'unclassified' ]; - const filteredTypes = [ 'unclassified' ]; +const TxTranslationType = ({ txTypes, isLoading, type }: Props) => { - if (!translatationType || filteredTypes.includes(translatationType)) { - return ; + if (!type || FILTERED_TYPES.includes(type.toLowerCase())) { + return ; } return ( - { camelCaseToSentence(translatationType) } + { camelCaseToSentence(type) } ); diff --git a/ui/txs/TxsContent.tsx b/ui/txs/TxsContent.tsx index 2153649c27..1b29786959 100644 --- a/ui/txs/TxsContent.tsx +++ b/ui/txs/TxsContent.tsx @@ -64,9 +64,9 @@ const TxsContent = ({ setSorting?.(value); }, [ sort, setSorting ]); - const itemsWithTranslation = useDescribeTxs(items, currentAddress, isPlaceholderData); + const translationQuery = useDescribeTxs(items, currentAddress, isPlaceholderData); - const content = itemsWithTranslation ? ( + const content = items && items.length > 0 ? ( <> @@ -117,7 +119,7 @@ const TxsContent = ({ return ( ; + translationQuery?: TxsTranslationQuery; } const TxsList = (props: Props) => { @@ -33,18 +35,22 @@ const TxsList = (props: Props) => { return ( { props.socketType && } - { props.items.slice(0, renderedItemsNum).map((tx, index) => ( - - )) } + { props.items.slice(0, renderedItemsNum).map((tx, index) => { + return ( + txHash.toLowerCase() === tx.hash.toLowerCase()) } + /> + ); + }) } ); diff --git a/ui/txs/TxsListItem.tsx b/ui/txs/TxsListItem.tsx index 55b8b9f07d..afe355f803 100644 --- a/ui/txs/TxsListItem.tsx +++ b/ui/txs/TxsListItem.tsx @@ -4,6 +4,7 @@ import { } from '@chakra-ui/react'; import React from 'react'; +import type { NovesDescribeTxsResponse } from 'types/api/noves'; import type { Transaction } from 'types/api/transaction'; import type { ClusterChainConfig } from 'types/multichain'; @@ -33,20 +34,32 @@ type Props = { isLoading?: boolean; animation?: string; chainData?: ClusterChainConfig; + translationIsLoading?: boolean; + translationData?: NovesDescribeTxsResponse; }; -const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeIncrement, animation, chainData }: Props) => { +const TxsListItem = ({ + tx, + isLoading, + showBlockInfo, + currentAddress, + enableTimeIncrement, + animation, + chainData, + translationIsLoading, + translationData, +}: Props) => { const dataTo = tx.to ? tx.to : tx.created_contract; return ( - { tx.translation ? ( + { translationIsLoading || translationData ? ( ) : diff --git a/ui/txs/TxsTable.tsx b/ui/txs/TxsTable.tsx index 877929772e..f0a23da209 100644 --- a/ui/txs/TxsTable.tsx +++ b/ui/txs/TxsTable.tsx @@ -12,6 +12,7 @@ import { currencyUnits } from 'lib/units'; import { TableBody, TableColumnHeader, TableColumnHeaderSortable, TableHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle'; +import type { TxsTranslationQuery } from './noves/useDescribeTxs'; import TxsSocketNotice from './socket/TxsSocketNotice'; import TxsTableItem from './TxsTableItem'; @@ -26,6 +27,7 @@ type Props = { enableTimeIncrement?: boolean; isLoading?: boolean; stickyHeader?: boolean; + translationQuery?: TxsTranslationQuery; }; const TxsTable = ({ @@ -39,6 +41,7 @@ const TxsTable = ({ enableTimeIncrement, isLoading, stickyHeader = true, + translationQuery, }: Props) => { const { cutRef, renderedItemsNum } = useLazyRenderedList(txs, !isLoading); const initialList = useInitialList({ @@ -118,18 +121,22 @@ const TxsTable = ({ { socketType && } - { txs.slice(0, renderedItemsNum).map((item, index) => ( - - )) } + { txs.slice(0, renderedItemsNum).map((item, index) => { + return ( + txHash.toLowerCase() === item.hash.toLowerCase()) } + /> + ); + }) }
diff --git a/ui/txs/TxsTableItem.tsx b/ui/txs/TxsTableItem.tsx index af62bcda22..c977907fd1 100644 --- a/ui/txs/TxsTableItem.tsx +++ b/ui/txs/TxsTableItem.tsx @@ -1,6 +1,7 @@ import { Flex, VStack } from '@chakra-ui/react'; import React from 'react'; +import type { NovesDescribeTxsResponse } from 'types/api/noves'; import type { Transaction } from 'types/api/transaction'; import type { ClusterChainConfig } from 'types/multichain'; @@ -30,9 +31,21 @@ type Props = { isLoading?: boolean; animation?: string; chainData?: ClusterChainConfig; + translationIsLoading?: boolean; + translationData?: NovesDescribeTxsResponse; }; -const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, isLoading, animation, chainData }: Props) => { +const TxsTableItem = ({ + tx, + showBlockInfo, + currentAddress, + enableTimeIncrement, + isLoading, + animation, + chainData, + translationIsLoading, + translationData, +}: Props) => { const dataTo = tx.to ? tx.to : tx.created_contract; return ( @@ -65,11 +78,11 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, - { tx.translation ? ( + { translationIsLoading || translationData ? ( ) : diff --git a/ui/txs/noves/useDescribeTxs.tsx b/ui/txs/noves/useDescribeTxs.tsx index 6566f55b89..db8ffa8de2 100644 --- a/ui/txs/noves/useDescribeTxs.tsx +++ b/ui/txs/noves/useDescribeTxs.tsx @@ -1,89 +1,45 @@ -import { useQuery } from '@tanstack/react-query'; import { uniq, chunk } from 'es-toolkit'; import React from 'react'; -import type { NovesDescribeTxsResponse } from 'types/api/noves'; import type { Transaction } from 'types/api/transaction'; import config from 'configs/app'; -import useApiFetch from 'lib/api/useApiFetch'; +import type { ReturnType } from 'lib/api/useApiQueries'; +import useApiQueries from 'lib/api/useApiQueries'; const feature = config.features.txInterpretation; const translateEnabled = feature.isEnabled && feature.provider === 'noves'; -export default function useDescribeTxs(items: Array | undefined, viewAsAccountAddress: string | undefined, isPlaceholderData: boolean) { - const apiFetch = useApiFetch(); - - const txsHash = items ? uniq(items.map(i => i.hash)) : []; - const txChunks = chunk(txsHash, 10); - - const queryKey = { - viewAsAccountAddress, - firstHash: txsHash[0] || '', - lastHash: txsHash[txsHash.length - 1] || '', - }; - - const describeQuery = useQuery({ - queryKey: [ 'general:noves_describe_txs', queryKey ], - queryFn: async() => { - const queries = txChunks.map((hashes) => { - if (hashes.length === 0) { - return Promise.resolve([]); - } - - return apiFetch('general:noves_describe_txs', { - queryParams: { - viewAsAccountAddress, - hashes, - }, - }) as Promise; - }); - - return Promise.all(queries); - }, - select: (data) => { - return data.flat(); - }, - enabled: translateEnabled && !isPlaceholderData, - }); - - const itemsWithTranslation = React.useMemo(() => items?.map(tx => { - const queryData = describeQuery.data; - const isLoading = describeQuery.isLoading; - - if (isLoading) { - return { - ...tx, - translation: { - isLoading, - }, - }; - } - - if (!queryData || !translateEnabled) { - return tx; +export type TxsTranslationQuery = ReturnType<'general:noves_describe_txs'> | undefined; + +export default function useDescribeTxs( + items: Array | undefined, + viewAsAccountAddress: string | undefined, + isPlaceholderData: boolean, +): TxsTranslationQuery { + const enabled = translateEnabled && !isPlaceholderData; + const chunks = React.useMemo(() => { + if (!enabled) { + return []; } - const query = queryData.find(data => data.txHash.toLowerCase() === tx.hash.toLowerCase()); + const txsHash = items ? uniq(items.map(({ hash }) => hash)) : []; + return chunk(txsHash, 10); + }, [ items, enabled ]); - if (query) { + const query = useApiQueries( + 'general:noves_describe_txs', + chunks.map((hashes) => { return { - ...tx, - translation: { - data: query, - isLoading: false, + queryParams: { + viewAsAccountAddress, + hashes, }, }; - } - - return tx; - }), [ items, describeQuery.data, describeQuery.isLoading ]); - - if (!translateEnabled || isPlaceholderData) { - return items; - } + }), + { enabled }, + ); - // return same "items" array of Transaction with a new "translation" field. - return itemsWithTranslation; + return enabled ? query : undefined; }