Skip to content

Commit

Permalink
feat: asset live transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
PatrickDinh authored May 16, 2024
1 parent b134b9e commit e48655c
Show file tree
Hide file tree
Showing 27 changed files with 351 additions and 106 deletions.
31 changes: 28 additions & 3 deletions src/features/assets/components/asset-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ import {
assetDefaultFrozenLabel,
assetDetailsLabel,
assetFreezeLabel,
assetHistoricalTransactionsTabId,
assetHistoricalTransactionsTabLabel,
assetIdLabel,
assetJsonLabel,
assetLiveTransactionsTabId,
assetLiveTransactionsTabLabel,
assetManagerLabel,
assetNameLabel,
assetReserveLabel,
Expand All @@ -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
Expand Down Expand Up @@ -157,9 +163,28 @@ export function AssetDetails({ asset }: Props) {
<Card className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<h1 className={cn('text-2xl text-primary font-bold')}>{assetTransactionsLabel}</h1>
<div className={cn('border-solid border-2 grid p-4')}>
<AssetTransactionHistory assetId={asset.id} />
</div>
<Tabs defaultValue={assetLiveTransactionsTabId}>
<TabsList aria-label={assetTransactionsLabel}>
<TabsTrigger
className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-48')}
value={assetLiveTransactionsTabId}
>
{assetLiveTransactionsTabLabel}
</TabsTrigger>
<TabsTrigger
className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-48')}
value={assetHistoricalTransactionsTabId}
>
{assetHistoricalTransactionsTabLabel}
</TabsTrigger>
</TabsList>
<OverflowAutoTabsContent value={assetLiveTransactionsTabId}>
<AssetLiveTransactions assetId={asset.id} />
</OverflowAutoTabsContent>
<OverflowAutoTabsContent value={assetHistoricalTransactionsTabId}>
<AssetTransactionHistory assetId={asset.id} />
</OverflowAutoTabsContent>
</Tabs>
</CardContent>
</Card>
</>
Expand Down
30 changes: 30 additions & 0 deletions src/features/assets/components/asset-live-transactions.tsx
Original file line number Diff line number Diff line change
@@ -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 <LiveTransactionsTable mapper={mapper} columns={assetTransactionsTableColumns} />
}
50 changes: 2 additions & 48 deletions src/features/assets/components/asset-transaction-history.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,45 +10,5 @@ type Props = {
export function AssetTransactionHistory({ assetId }: Props) {
const fetchNextPage = useFetchNextAssetTransactionsPage(assetId)

return <LazyLoadDataTable columns={transactionsTableColumns} fetchNextPage={fetchNextPage} />
return <LazyLoadDataTable columns={assetTransactionsTableColumns} fetchNextPage={fetchNextPage} />
}

const transactionsTableColumns: ColumnDef<Transaction>[] = [
{
header: 'Transaction Id',
accessorKey: 'id',
cell: (c) => {
const value = c.getValue<string>()
return <TransactionLink transactionId={value} short={true} />
},
},
{
accessorKey: 'sender',
header: 'From',
cell: (c) => ellipseAddress(c.getValue<string>()),
},
{
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<Transaction>()
if (transaction.type === TransactionType.Payment) return <DisplayAlgo className={cn('justify-center')} amount={transaction.amount} />
if (transaction.type === TransactionType.AssetTransfer)
return <DisplayAssetAmount amount={transaction.amount} asset={transaction.asset} />
},
},
]
4 changes: 4 additions & 0 deletions src/features/assets/components/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
31 changes: 17 additions & 14 deletions src/features/assets/data/asset-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/features/assets/data/asset-transaction-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
})
Expand Down
8 changes: 8 additions & 0 deletions src/features/assets/pages/asset-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
assetReserveLabel,
assetTotalSupplyLabel,
assetTraitsLabel,
assetTransactionsLabel,
assetUrlLabel,
} from '../components/labels'
import { useParams } from 'react-router-dom'
Expand Down Expand Up @@ -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)
})
}
)
Expand Down Expand Up @@ -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()
})
}
)
Expand Down
57 changes: 57 additions & 0 deletions src/features/assets/utils/asset-transactions-table-columns.tsx
Original file line number Diff line number Diff line change
@@ -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<Transaction | InnerTransaction>[] = [
{
header: 'Transaction Id',
accessorFn: (transaction) => transaction,
cell: (c) => {
const transaction = c.getValue<Transaction | InnerTransaction>()
return 'innerId' in transaction ? (
<TransactionLink
className={cn('text-primary underline cursor-pointer grid gap-2')}
transactionId={transaction.networkTransactionId}
>
<span>{ellipseId(transaction.id)}</span>
<span>(Inner)</span>
</TransactionLink>
) : (
<TransactionLink transactionId={transaction.id} short={true} />
)
},
},
{
header: 'Round',
accessorKey: 'confirmedRound',
},
{
accessorKey: 'sender',
header: 'From',
cell: (c) => ellipseAddress(c.getValue<string>()),
},
{
header: 'To',
accessorFn: asTo,
},
{
accessorKey: 'type',
header: 'Type',
},
{
header: 'Amount',
accessorFn: (transaction) => transaction,
cell: (c) => {
const transaction = c.getValue<Transaction>()
if (transaction.type === TransactionType.Payment) return <DisplayAlgo className={cn('justify-center')} amount={transaction.amount} />
if (transaction.type === TransactionType.AssetTransfer)
return <DisplayAssetAmount amount={transaction.amount} asset={transaction.asset} />
},
},
]
20 changes: 20 additions & 0 deletions src/features/assets/utils/extract-transactions-for-asset.ts
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 2 additions & 7 deletions src/features/blocks/components/transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -31,13 +32,7 @@ export const columns: ColumnDef<Transaction>[] = [
},
{
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',
Expand Down
4 changes: 2 additions & 2 deletions src/features/blocks/data/block-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
})()
})

Expand Down
Loading

0 comments on commit e48655c

Please sign in to comment.