diff --git a/src/features/assets/components/asset-details.tsx b/src/features/assets/components/asset-details.tsx index e992e4575..3e50d355a 100644 --- a/src/features/assets/components/asset-details.tsx +++ b/src/features/assets/components/asset-details.tsx @@ -15,8 +15,12 @@ import { assetDefaultFrozenLabel, assetDetailsLabel, assetFreezeLabel, + assetHistoricalTransactionsTabId, + assetHistoricalTransactionsTabLabel, assetIdLabel, assetJsonLabel, + assetLiveTransactionsTabId, + assetLiveTransactionsTabLabel, assetManagerLabel, assetNameLabel, assetReserveLabel, @@ -30,6 +34,8 @@ import { AssetMedia } from './asset-media' import { AssetTraits } from './asset-traits' import { AssetMetadata } from './asset-metadata' import { AssetTransactionHistory } from './asset-transaction-history' +import { AssetLiveTransactions } from './asset-live-transactions' +import { OverflowAutoTabsContent, Tabs, TabsList, TabsTrigger } from '@/features/common/components/tabs' type Props = { asset: Asset @@ -157,9 +163,28 @@ export function AssetDetails({ asset }: Props) {

{assetTransactionsLabel}

-
- -
+ + + + {assetLiveTransactionsTabLabel} + + + {assetHistoricalTransactionsTabLabel} + + + + + + + + +
diff --git a/src/features/assets/components/asset-live-transactions.tsx b/src/features/assets/components/asset-live-transactions.tsx new file mode 100644 index 000000000..e9f78223e --- /dev/null +++ b/src/features/assets/components/asset-live-transactions.tsx @@ -0,0 +1,30 @@ +import { AssetId } from '../data/types' +import { useCallback } from 'react' +import { LiveTransactionsTable } from '@/features/transactions/components/live-transactions-table' +import { assetTransactionsTableColumns } from '../utils/asset-transactions-table-columns' +import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { JotaiStore } from '@/features/common/data/types' +import { createTransactionAtom } from '@/features/transactions/data' +import { atom } from 'jotai' +import { getAssetIdsForTransaction } from '@/features/transactions/utils/get-asset-ids-for-transaction' +import { extractTransactionsForAsset } from '../utils/extract-transactions-for-asset' + +type Props = { + assetId: AssetId +} + +export function AssetLiveTransactions({ assetId }: Props) { + const mapper = useCallback( + (store: JotaiStore, transactionResult: TransactionResult) => { + return atom(async (get) => { + const assetIdsForTransaction = getAssetIdsForTransaction(transactionResult) + if (!assetIdsForTransaction.includes(assetId)) return [] + + const transaction = await get(createTransactionAtom(store, transactionResult)) + return extractTransactionsForAsset(transaction, assetId) + }) + }, + [assetId] + ) + return +} diff --git a/src/features/assets/components/asset-transaction-history.tsx b/src/features/assets/components/asset-transaction-history.tsx index eb81ec7c6..3b3df1f9d 100644 --- a/src/features/assets/components/asset-transaction-history.tsx +++ b/src/features/assets/components/asset-transaction-history.tsx @@ -1,13 +1,7 @@ import { LazyLoadDataTable } from '@/features/common/components/lazy-load-data-table' import { AssetId } from '../data/types' -import { Transaction, TransactionType } from '@/features/transactions/models' -import { DisplayAlgo } from '@/features/common/components/display-algo' -import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount' -import { cn } from '@/features/common/utils' -import { TransactionLink } from '@/features/transactions/components/transaction-link' -import { ellipseAddress } from '@/utils/ellipse-address' -import { ColumnDef } from '@tanstack/react-table' import { useFetchNextAssetTransactionsPage } from '../data/asset-transaction-history' +import { assetTransactionsTableColumns } from '../utils/asset-transactions-table-columns' type Props = { assetId: AssetId @@ -16,45 +10,5 @@ type Props = { export function AssetTransactionHistory({ assetId }: Props) { const fetchNextPage = useFetchNextAssetTransactionsPage(assetId) - return + return } - -const transactionsTableColumns: ColumnDef[] = [ - { - header: 'Transaction Id', - accessorKey: 'id', - cell: (c) => { - const value = c.getValue() - return - }, - }, - { - accessorKey: 'sender', - header: 'From', - cell: (c) => ellipseAddress(c.getValue()), - }, - { - header: 'To', - accessorFn: (transaction) => { - if (transaction.type === TransactionType.Payment || transaction.type === TransactionType.AssetTransfer) - return ellipseAddress(transaction.receiver) - if (transaction.type === TransactionType.ApplicationCall) return transaction.applicationId - if (transaction.type === TransactionType.AssetConfig) return transaction.assetId - if (transaction.type === TransactionType.AssetFreeze) return ellipseAddress(transaction.address) - }, - }, - { - accessorKey: 'type', - header: 'Type', - }, - { - header: 'Amount', - accessorFn: (transaction) => transaction, - cell: (c) => { - const transaction = c.getValue() - if (transaction.type === TransactionType.Payment) return - if (transaction.type === TransactionType.AssetTransfer) - return - }, - }, -] diff --git a/src/features/assets/components/labels.ts b/src/features/assets/components/labels.ts index 443c9dffd..91a7e0b54 100644 --- a/src/features/assets/components/labels.ts +++ b/src/features/assets/components/labels.ts @@ -23,3 +23,7 @@ export const assetMetadataLabel = 'Asset Metadata' export const assetJsonLabel = 'Asset JSON' export const assetTransactionsLabel = 'Asset Transactions' +export const assetLiveTransactionsTabId = 'live-transactions' +export const assetLiveTransactionsTabLabel = 'Live Transactions' +export const assetHistoricalTransactionsTabId = 'historical-transactions' +export const assetHistoricalTransactionsTabLabel = 'Historical Transactions' diff --git a/src/features/assets/data/asset-metadata.ts b/src/features/assets/data/asset-metadata.ts index 28a720927..4f3af4187 100644 --- a/src/features/assets/data/asset-metadata.ts +++ b/src/features/assets/data/asset-metadata.ts @@ -75,7 +75,7 @@ export const createAssetMetadataResultAtom = (assetResult: AssetResult) => return null } - const results = + let results = assetResult.params.manager && assetResult.params.manager !== ZERO_ADDRESS ? await indexer .searchForTransactions() @@ -85,19 +85,22 @@ export const createAssetMetadataResultAtom = (assetResult: AssetResult) => .addressRole('sender') .limit(2) // Return 2 to cater for a destroy transaction and any potential eventual consistency delays between transactions and assets. .do() - .then((res) => res.transactions as TransactionResult[]) // Implicitly newest to oldest when filtering with an address - : // The asset has been destroyed or is an immutable asset. - // Fetch the entire acfg transaction history and reverse the order, so it's newest to oldest - await executePaginatedRequest( - (res: TransactionSearchResults) => res.transactions, - (nextToken) => { - let s = indexer.searchForTransactions().assetID(assetResult.index).txType('acfg') - if (nextToken) { - s = s.nextToken(nextToken) - } - return s - } - ).then((res) => res.reverse()) // reverse the order, so it's newest to oldest + .then((res) => res.transactions as TransactionResult[]) // Implicitly newest to oldest when filtering with an address. + : [] + if (results.length === 0) { + // The asset has been destroyed, is an immutable asset, or the asset is mutable however has never been mutated. + // Fetch the entire acfg transaction history and reverse the order, so it's newest to oldest. + results = await executePaginatedRequest( + (res: TransactionSearchResults) => res.transactions, + (nextToken) => { + let s = indexer.searchForTransactions().assetID(assetResult.index).txType('acfg') + if (nextToken) { + s = s.nextToken(nextToken) + } + return s + } + ).then((res) => res.reverse()) // reverse the order, so it's newest to oldest + } const assetConfigTransactionResults = results.flatMap(flattenTransactionResult).filter((t) => { const isAssetConfigTransaction = t['tx-type'] === TransactionType.acfg diff --git a/src/features/assets/data/asset-transaction-history.ts b/src/features/assets/data/asset-transaction-history.ts index f5f41078e..7f6825176 100644 --- a/src/features/assets/data/asset-transaction-history.ts +++ b/src/features/assets/data/asset-transaction-history.ts @@ -6,6 +6,7 @@ import { JotaiStore } from '@/features/common/data/types' import { createTransactionsAtom, transactionResultsAtom } from '@/features/transactions/data' import { atomEffect } from 'jotai-effect' import { atom, useStore } from 'jotai' +import { extractTransactionsForAsset } from '../utils/extract-transactions-for-asset' const fetchAssetTransactionResults = async (assetId: AssetId, pageSize: number, nextPageToken?: string) => { const results = (await indexer @@ -47,9 +48,10 @@ const createAssetTransactionsAtom = (store: JotaiStore, assetId: AssetId, pageSi get(createSyncEffect(transactionResults)) const transactions = await get(createTransactionsAtom(store, transactionResults)) + const transactionsForAsset = transactions.flatMap((transaction) => extractTransactionsForAsset(transaction, assetId)) return { - rows: transactions, + rows: transactionsForAsset, nextPageToken: newNextPageToken, } }) diff --git a/src/features/assets/pages/asset-page.test.tsx b/src/features/assets/pages/asset-page.test.tsx index 94402ec83..0810ce51d 100644 --- a/src/features/assets/pages/asset-page.test.tsx +++ b/src/features/assets/pages/asset-page.test.tsx @@ -17,6 +17,7 @@ import { assetReserveLabel, assetTotalSupplyLabel, assetTraitsLabel, + assetTransactionsLabel, assetUrlLabel, } from '../components/labels' import { useParams } from 'react-router-dom' @@ -147,6 +148,10 @@ describe('asset-page', () => { { term: 'Image Mimetype', description: 'image/png' }, ], }) + + const transactionTabList = component.getByRole('tablist', { name: assetTransactionsLabel }) + expect(transactionTabList).toBeTruthy() + expect(transactionTabList.children.length).toBe(2) }) } ) @@ -608,6 +613,9 @@ describe('asset-page', () => { const assetTraitsCard = component.queryByText(assetTraitsLabel) expect(assetTraitsCard).toBeNull() + + const transactionTabList = component.queryByRole('tablist', { name: assetTransactionsLabel }) + expect(transactionTabList).toBeNull() }) } ) diff --git a/src/features/assets/utils/asset-transactions-table-columns.tsx b/src/features/assets/utils/asset-transactions-table-columns.tsx new file mode 100644 index 000000000..454e40b40 --- /dev/null +++ b/src/features/assets/utils/asset-transactions-table-columns.tsx @@ -0,0 +1,57 @@ +import { InnerTransaction, Transaction, TransactionType } from '@/features/transactions/models' +import { cn } from '@/features/common/utils' +import { ellipseAddress } from '@/utils/ellipse-address' +import { ColumnDef } from '@tanstack/react-table' +import { DisplayAlgo } from '@/features/common/components/display-algo' +import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount' +import { TransactionLink } from '@/features/transactions/components/transaction-link' +import { ellipseId } from '@/utils/ellipse-id' +import { asTo } from '@/features/common/mappers/to' + +export const assetTransactionsTableColumns: ColumnDef[] = [ + { + header: 'Transaction Id', + accessorFn: (transaction) => transaction, + cell: (c) => { + const transaction = c.getValue() + return 'innerId' in transaction ? ( + + {ellipseId(transaction.id)} + (Inner) + + ) : ( + + ) + }, + }, + { + header: 'Round', + accessorKey: 'confirmedRound', + }, + { + accessorKey: 'sender', + header: 'From', + cell: (c) => ellipseAddress(c.getValue()), + }, + { + header: 'To', + accessorFn: asTo, + }, + { + accessorKey: 'type', + header: 'Type', + }, + { + header: 'Amount', + accessorFn: (transaction) => transaction, + cell: (c) => { + const transaction = c.getValue() + if (transaction.type === TransactionType.Payment) return + if (transaction.type === TransactionType.AssetTransfer) + return + }, + }, +] diff --git a/src/features/assets/utils/extract-transactions-for-asset.ts b/src/features/assets/utils/extract-transactions-for-asset.ts new file mode 100644 index 000000000..4ab154bad --- /dev/null +++ b/src/features/assets/utils/extract-transactions-for-asset.ts @@ -0,0 +1,20 @@ +import { Transaction, TransactionType } from '@/features/transactions/models' +import { flattenInnerTransactions } from '@/utils/flatten-inner-transactions' + +export const extractTransactionsForAsset = (transaction: Transaction, assetIndex: number) => { + const flattenedTransactions = flattenInnerTransactions(transaction) + const results = [] + + for (const { transaction } of flattenedTransactions) { + if (transaction.type === TransactionType.AssetConfig && transaction.assetId === assetIndex) { + results.push(transaction) + } + if (transaction.type === TransactionType.AssetTransfer && transaction.asset.id === assetIndex) { + results.push(transaction) + } + if (transaction.type === TransactionType.AssetFreeze && transaction.assetId === assetIndex) { + results.push(transaction) + } + } + return results +} diff --git a/src/features/blocks/components/transactions.tsx b/src/features/blocks/components/transactions.tsx index b66a061aa..1e00ccbdc 100644 --- a/src/features/blocks/components/transactions.tsx +++ b/src/features/blocks/components/transactions.tsx @@ -6,6 +6,7 @@ import { DataTable } from '@/features/common/components/data-table' import { TransactionLink } from '@/features/transactions/components/transaction-link' import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount' import { GroupLink } from '@/features/groups/components/group-link' +import { asTo } from '@/features/common/mappers/to' type Props = { transactions: Transaction[] @@ -31,13 +32,7 @@ export const columns: ColumnDef[] = [ }, { header: 'To', - accessorFn: (transaction) => { - if (transaction.type === TransactionType.Payment || transaction.type === TransactionType.AssetTransfer) - return ellipseAddress(transaction.receiver) - if (transaction.type === TransactionType.ApplicationCall) return transaction.applicationId - if (transaction.type === TransactionType.AssetConfig) return transaction.assetId - if (transaction.type === TransactionType.AssetFreeze) return transaction.assetId - }, + accessorFn: asTo, }, { accessorKey: 'type', diff --git a/src/features/blocks/data/block-result.ts b/src/features/blocks/data/block-result.ts index 358675199..6efe7c4b9 100644 --- a/src/features/blocks/data/block-result.ts +++ b/src/features/blocks/data/block-result.ts @@ -50,7 +50,7 @@ export const createBlockExtractAtom = (round: Round) => { }) } -export const addStateExtractFromBlocksAtom = atom( +export const addStateExtractedFromBlocksAtom = atom( null, (get, set, blockResults: BlockResult[], transactionResults: TransactionResult[], groupResults: GroupResult[]) => { if (transactionResults.length > 0) { @@ -108,7 +108,7 @@ const createBlockResultAtom = (round: Round) => { }) // Don't need to sync the block, as it's synced by atomsInAtom, due to this atom returning the block - set(addStateExtractFromBlocksAtom, [], transactionResults, groupResults) + set(addStateExtractedFromBlocksAtom, [], transactionResults, groupResults) })() }) diff --git a/src/features/blocks/data/latest-blocks.ts b/src/features/blocks/data/latest-blocks.ts index 5bf5561ce..eb24997a2 100644 --- a/src/features/blocks/data/latest-blocks.ts +++ b/src/features/blocks/data/latest-blocks.ts @@ -1,8 +1,8 @@ import { atom, useAtom, useAtomValue } from 'jotai' import { isDefined } from '@/utils/is-defined' import { asBlockSummary } from '../mappers' -import { transactionResultsAtom } from '@/features/transactions/data' -import { asTransactionSummary } from '@/features/transactions/mappers/transaction-mappers' +import { liveTransactionIdsAtom, transactionResultsAtom } from '@/features/transactions/data' +import { asTransactionSummary } from '@/features/transactions/mappers' import { atomEffect } from 'jotai-effect' import { AlgorandSubscriber } from '@algorandfoundation/algokit-subscriber' import { algod } from '@/features/common/data' @@ -14,7 +14,7 @@ import { flattenTransactionResult } from '@/features/transactions/utils/flatten- import { distinct } from '@/utils/distinct' import { assetResultsAtom } from '@/features/assets/data' import { BlockSummary } from '../models' -import { blockResultsAtom, addStateExtractFromBlocksAtom, syncedRoundAtom } from './block-result' +import { blockResultsAtom, addStateExtractedFromBlocksAtom, syncedRoundAtom } from './block-result' import { GroupId, GroupResult } from '@/features/groups/data/types' import { AssetId } from '@/features/assets/data/types' @@ -161,7 +161,11 @@ const subscribeToBlocksEffect = atomEffect((get, set) => { }) } - set(addStateExtractFromBlocksAtom, blockResults, transactionResults, Array.from(groupResults.values())) + set(addStateExtractedFromBlocksAtom, blockResults, transactionResults, Array.from(groupResults.values())) + + set(liveTransactionIdsAtom, (prev) => { + return transactionResults.map((txn) => txn.id).concat(prev) + }) }) subscriber.start() diff --git a/src/features/blocks/mappers/index.ts b/src/features/blocks/mappers/index.ts index 9cca97327..3e8feb725 100644 --- a/src/features/blocks/mappers/index.ts +++ b/src/features/blocks/mappers/index.ts @@ -1,7 +1,7 @@ import { Transaction, TransactionSummary } from '@/features/transactions/models' import { Block, BlockSummary, CommonBlockProperties } from '../models' import { BlockResult } from '../data/types' -import { asTransactionsSummary } from '@/features/common/mappers' +import { asTransactionsSummary } from '@/features/transactions/mappers' const asCommonBlock = (block: BlockResult, transactions: Pick[]): CommonBlockProperties => { return { diff --git a/src/features/common/components/command.tsx b/src/features/common/components/command.tsx index 9bba2bb5d..e1cd46280 100644 --- a/src/features/common/components/command.tsx +++ b/src/features/common/components/command.tsx @@ -1,5 +1,3 @@ -'use client' - import * as React from 'react' import { type DialogProps } from '@radix-ui/react-dialog' import { MagnifyingGlassIcon } from '@radix-ui/react-icons' diff --git a/src/features/common/components/data-table.tsx b/src/features/common/components/data-table.tsx index d645c6f9b..83e2b2eba 100644 --- a/src/features/common/components/data-table.tsx +++ b/src/features/common/components/data-table.tsx @@ -1,5 +1,3 @@ -'use client' - import { ColumnDef, flexRender, getCoreRowModel, useReactTable, getPaginationRowModel } from '@tanstack/react-table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/features/common/components/table' import { DataTablePagination } from './data-table-pagination' diff --git a/src/features/common/components/dialog.tsx b/src/features/common/components/dialog.tsx index 83f67da3a..e6790fd0a 100644 --- a/src/features/common/components/dialog.tsx +++ b/src/features/common/components/dialog.tsx @@ -1,5 +1,3 @@ -'use client' - import * as React from 'react' import * as DialogPrimitive from '@radix-ui/react-dialog' import { Cross2Icon } from '@radix-ui/react-icons' diff --git a/src/features/common/mappers/to.ts b/src/features/common/mappers/to.ts new file mode 100644 index 000000000..8fd70de61 --- /dev/null +++ b/src/features/common/mappers/to.ts @@ -0,0 +1,10 @@ +import { Transaction, TransactionType } from '@/features/transactions/models' +import { ellipseAddress } from '@/utils/ellipse-address' + +export const asTo = (transaction: Transaction) => { + if (transaction.type === TransactionType.Payment || transaction.type === TransactionType.AssetTransfer) + return ellipseAddress(transaction.receiver) + if (transaction.type === TransactionType.ApplicationCall) return transaction.applicationId + if (transaction.type === TransactionType.AssetConfig) return transaction.assetId + if (transaction.type === TransactionType.AssetFreeze) return ellipseAddress(transaction.address) +} diff --git a/src/features/groups/data/group-result.ts b/src/features/groups/data/group-result.ts index 57a0b543f..6333b4b66 100644 --- a/src/features/groups/data/group-result.ts +++ b/src/features/groups/data/group-result.ts @@ -1,7 +1,7 @@ import { Round } from '@/features/blocks/data/types' import { atom } from 'jotai' import { GroupId, GroupResult } from './types' -import { createBlockExtractAtom, addStateExtractFromBlocksAtom as addStateExtractedFromBlocksAtom } from '@/features/blocks/data' +import { createBlockExtractAtom, addStateExtractedFromBlocksAtom } from '@/features/blocks/data' import { invariant } from '@/utils/invariant' import { atomEffect } from 'jotai-effect' import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' diff --git a/src/features/groups/mappers/index.ts b/src/features/groups/mappers/index.ts index b00493053..261ef8a74 100644 --- a/src/features/groups/mappers/index.ts +++ b/src/features/groups/mappers/index.ts @@ -1,7 +1,7 @@ import { Group } from '../models' import { GroupResult } from '../data/types' -import { asTransactionsSummary } from '@/features/common/mappers' import { Transaction } from '@/features/transactions/models' +import { asTransactionsSummary } from '@/features/transactions/mappers' export const asGroup = (groupResult: GroupResult, transactions: Transaction[]): Group => { return { diff --git a/src/features/transactions/components/live-transactions-table.tsx b/src/features/transactions/components/live-transactions-table.tsx new file mode 100644 index 000000000..b19262228 --- /dev/null +++ b/src/features/transactions/components/live-transactions-table.tsx @@ -0,0 +1,85 @@ +import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/features/common/components/table' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../common/components/select' +import { useState } from 'react' +import { InnerTransaction, Transaction } from '@/features/transactions/models' +import { Atom } from 'jotai' +import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { JotaiStore } from '@/features/common/data/types' +import { useLiveTransactions } from '../data/live-transaction' + +interface Props { + columns: ColumnDef[] + mapper: (store: JotaiStore, transactionResult: TransactionResult) => Atom> +} + +export function LiveTransactionsTable({ mapper, columns }: Props) { + const [maxRows, setMaxRows] = useState(10) + const transactions = useLiveTransactions(mapper, maxRows) + const table = useReactTable({ + data: transactions, + columns, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+
+

Max rows

+ +
+
+
+
+ ) +} + +const maxRowsOptions = [10, 20, 30, 40, 50] diff --git a/src/features/transactions/components/transactions-table.tsx b/src/features/transactions/components/transactions-table.tsx index 532124e69..10a43f896 100644 --- a/src/features/transactions/components/transactions-table.tsx +++ b/src/features/transactions/components/transactions-table.tsx @@ -9,6 +9,7 @@ import { ColumnDef } from '@tanstack/react-table' import { DataTable } from '@/features/common/components/data-table' import { InnerTransactionLink } from './inner-transaction-link' import { TransactionLink } from './transaction-link' +import { asTo } from '@/features/common/mappers/to' const graphConfig = { indentationWidth: 20, @@ -50,15 +51,7 @@ export const transactionsTableColumns: ColumnDef[] = [ }, { header: 'To', - accessorFn: (item) => item.transaction, - cell: (c) => { - const transaction = c.getValue() - if (transaction.type === TransactionType.Payment || transaction.type === TransactionType.AssetTransfer) - return ellipseAddress(transaction.receiver) - if (transaction.type === TransactionType.ApplicationCall) return transaction.applicationId - if (transaction.type === TransactionType.AssetConfig) return transaction.assetId - if (transaction.type === TransactionType.AssetFreeze) return ellipseAddress(transaction.address) - }, + accessorFn: (item) => asTo(item.transaction), }, { accessorKey: 'transaction.type', diff --git a/src/features/transactions/data/index.ts b/src/features/transactions/data/index.ts index 256b36830..630d34da4 100644 --- a/src/features/transactions/data/index.ts +++ b/src/features/transactions/data/index.ts @@ -3,3 +3,4 @@ export * from './transaction' export * from './latest-transactions' export * from './inner-transaction' export * from './logicsig-teal' +export * from './live-transaction-ids' diff --git a/src/features/transactions/data/live-transaction-ids.ts b/src/features/transactions/data/live-transaction-ids.ts new file mode 100644 index 000000000..f7fb838c7 --- /dev/null +++ b/src/features/transactions/data/live-transaction-ids.ts @@ -0,0 +1,4 @@ +import { atom } from 'jotai' +import { TransactionId } from './types' + +export const liveTransactionIdsAtom = atom([]) diff --git a/src/features/transactions/data/live-transaction.ts b/src/features/transactions/data/live-transaction.ts new file mode 100644 index 000000000..effa69d7e --- /dev/null +++ b/src/features/transactions/data/live-transaction.ts @@ -0,0 +1,57 @@ +import { useMemo } from 'react' +import { getTransactionResultAtom, liveTransactionIdsAtom } from '@/features/transactions/data' +import { InnerTransaction, Transaction } from '@/features/transactions/models' +import { atomEffect } from 'jotai-effect' +import { Atom, atom, useAtom, useAtomValue, useStore } from 'jotai' +import { TransactionId } from '@/features/transactions/data/types' +import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { JotaiStore } from '@/features/common/data/types' + +export const useLiveTransactions = ( + mapper: (store: JotaiStore, transactionResult: TransactionResult) => Atom>, + maxRows: number +) => { + const store = useStore() + + const { liveTransactionsAtomEffect, liveTransactionsAtom } = useMemo(() => { + let syncedTransactionId: TransactionId | undefined = undefined + const liveTransactionsAtom = atom<(Transaction | InnerTransaction)[]>([]) + + const liveTransactionsAtomEffect = atomEffect((get, set) => { + ;(async () => { + const liveTransactionIds = get(liveTransactionIdsAtom) + + const newTransactionResults: TransactionResult[] = [] + for (const transactionId of liveTransactionIds) { + if (transactionId === syncedTransactionId) { + break + } + const transactionResultAtom = getTransactionResultAtom(store, transactionId) + + const transactionResult = await get.peek(transactionResultAtom) + newTransactionResults.push(transactionResult) + } + syncedTransactionId = liveTransactionIds[0] + + const newTransactions = ( + await Promise.all(newTransactionResults.map((transactionResult) => get(mapper(store, transactionResult)))) + ).flat() + if (newTransactions.length) { + set(liveTransactionsAtom, (prev) => { + return newTransactions.concat(prev).slice(0, maxRows) + }) + } + })() + }) + + return { + liveTransactionsAtomEffect, + liveTransactionsAtom, + } + }, [store, mapper, maxRows]) + + useAtom(liveTransactionsAtomEffect) + + const transactions = useAtomValue(liveTransactionsAtom) + return transactions +} diff --git a/src/features/transactions/mappers/index.ts b/src/features/transactions/mappers/index.ts index 3fd158bd1..fd5e951b6 100644 --- a/src/features/transactions/mappers/index.ts +++ b/src/features/transactions/mappers/index.ts @@ -2,3 +2,4 @@ export * from './payment-transaction-mappers' export * from './asset-transfer-transaction-mappers' export * from './app-call-transaction-mappers' export * from './transaction-mappers' +export * from './transaction-summary' diff --git a/src/features/common/mappers/index.ts b/src/features/transactions/mappers/transaction-summary.ts similarity index 89% rename from src/features/common/mappers/index.ts rename to src/features/transactions/mappers/transaction-summary.ts index 6c06f169c..704fb5247 100644 --- a/src/features/common/mappers/index.ts +++ b/src/features/transactions/mappers/transaction-summary.ts @@ -1,5 +1,5 @@ +import { TransactionsSummary } from '@/features/common/models' import { Transaction, TransactionType } from '@/features/transactions/models' -import { TransactionsSummary } from '../models' export const asTransactionsSummary = (transactions: Pick[]): TransactionsSummary => { return { diff --git a/src/features/transactions/utils/get-asset-ids-for-transaction.ts b/src/features/transactions/utils/get-asset-ids-for-transaction.ts index 018101213..d21fdc145 100644 --- a/src/features/transactions/utils/get-asset-ids-for-transaction.ts +++ b/src/features/transactions/utils/get-asset-ids-for-transaction.ts @@ -1,8 +1,9 @@ import algosdk from 'algosdk' import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' import { invariant } from '@/utils/invariant' +import { AssetId } from '@/features/assets/data/types' -export const getAssetIdsForTransaction = (transaction: TransactionResult): number[] => { +export const getAssetIdsForTransaction = (transaction: TransactionResult): AssetId[] => { if (transaction['tx-type'] === algosdk.TransactionType.axfer) { invariant(transaction['asset-transfer-transaction'], 'asset-transfer-transaction is not set') @@ -12,13 +13,10 @@ export const getAssetIdsForTransaction = (transaction: TransactionResult): numbe invariant(transaction['application-transaction'], 'application-transaction is not set') const innerTransactions = transaction['inner-txns'] ?? [] - return innerTransactions.reduce( - (acc, innerTxn) => { - const innerResult = getAssetIdsForTransaction(innerTxn) - return acc.concat(innerResult) - }, - transaction['application-transaction']['foreign-assets'] ?? ([] as number[]) - ) + return innerTransactions.reduce((acc, innerTxn) => { + const innerResult = getAssetIdsForTransaction(innerTxn) + return acc.concat(innerResult) + }, [] as number[]) } if (transaction['tx-type'] === algosdk.TransactionType.acfg) { invariant(transaction['asset-config-transaction'], 'asset-config-transaction is not set')