Skip to content

Commit 3aafc0d

Browse files
authored
feat: view application transactions
1 parent 1da3e2a commit 3aafc0d

17 files changed

+312
-73
lines changed

src/features/applications/components/application-details.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,22 @@ import {
1515
applicationGlobalStateByteLabel,
1616
applicationGlobalStateLabel,
1717
applicationGlobalStateUintLabel,
18+
applicationHistoricalTransactionsTabId,
19+
applicationHistoricalTransactionsTabLabel,
1820
applicationIdLabel,
21+
applicationLiveTransactionsTabId,
22+
applicationLiveTransactionsTabLabel,
1923
applicationLocalStateByteLabel,
2024
applicationLocalStateUintLabel,
25+
applicationTransactionsLabel,
2126
} from './labels'
2227
import { isDefined } from '@/utils/is-defined'
2328
import { ApplicationProgram } from './application-program'
2429
import { ApplicationGlobalStateTable } from './application-global-state-table'
2530
import { ApplicationBoxes } from './application-boxes'
31+
import { OverflowAutoTabsContent, Tabs, TabsList, TabsTrigger } from '@/features/common/components/tabs'
32+
import { ApplicationLiveTransactions } from './application-live-transactions'
33+
import { ApplicationTransactionHistory } from './application-transaction-history'
2634

2735
type Props = {
2836
application: Application
@@ -106,6 +114,33 @@ export function ApplicationDetails({ application }: Props) {
106114
<ApplicationBoxes applicationId={application.id} />
107115
</CardContent>
108116
</Card>
117+
<Card aria-label={applicationTransactionsLabel} className={cn('p-4')}>
118+
<CardContent className={cn('text-sm space-y-2')}>
119+
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationTransactionsLabel}</h1>
120+
<Tabs defaultValue={applicationLiveTransactionsTabId}>
121+
<TabsList aria-label={applicationTransactionsLabel}>
122+
<TabsTrigger
123+
className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-48')}
124+
value={applicationLiveTransactionsTabId}
125+
>
126+
{applicationLiveTransactionsTabLabel}
127+
</TabsTrigger>
128+
<TabsTrigger
129+
className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-48')}
130+
value={applicationHistoricalTransactionsTabId}
131+
>
132+
{applicationHistoricalTransactionsTabLabel}
133+
</TabsTrigger>
134+
</TabsList>
135+
<OverflowAutoTabsContent value={applicationLiveTransactionsTabId}>
136+
<ApplicationLiveTransactions applicationId={application.id} />
137+
</OverflowAutoTabsContent>
138+
<OverflowAutoTabsContent value={applicationHistoricalTransactionsTabId}>
139+
<ApplicationTransactionHistory applicationId={application.id} />
140+
</OverflowAutoTabsContent>
141+
</Tabs>
142+
</CardContent>
143+
</Card>
109144
</div>
110145
)
111146
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ApplicationId } from '../data/types'
2+
import { useCallback } from 'react'
3+
import { LiveTransactionsTable } from '@/features/transactions/components/live-transactions-table'
4+
import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
5+
import { flattenTransactionResult } from '@/features/transactions/utils/flatten-transaction-result'
6+
import { TransactionType as AlgoSdkTransactionType } from 'algosdk'
7+
import { applicationTransactionsTableColumns } from '../utils/application-transactions-table-columns'
8+
import { Transaction, InnerTransaction } from '@/features/transactions/models'
9+
import { getApplicationTransactionsTableSubRows } from '../utils/get-application-transactions-table-sub-rows'
10+
11+
type Props = {
12+
applicationId: ApplicationId
13+
}
14+
15+
export function ApplicationLiveTransactions({ applicationId }: Props) {
16+
const filter = useCallback(
17+
(transactionResult: TransactionResult) => {
18+
const flattenedTransactionResults = flattenTransactionResult(transactionResult)
19+
return flattenedTransactionResults.some(
20+
(txn) => txn['tx-type'] === AlgoSdkTransactionType.appl && txn['application-transaction']?.['application-id'] === applicationId
21+
)
22+
},
23+
[applicationId]
24+
)
25+
26+
const getSubRows = useCallback(
27+
(row: Transaction | InnerTransaction) => getApplicationTransactionsTableSubRows(applicationId, row),
28+
[applicationId]
29+
)
30+
31+
return <LiveTransactionsTable filter={filter} getSubRows={getSubRows} columns={applicationTransactionsTableColumns} />
32+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { LazyLoadDataTable } from '@/features/common/components/lazy-load-data-table'
2+
import { ApplicationId } from '../data/types'
3+
import { useFetchNextApplicationTransactionsPage } from '../data/application-transaction-history'
4+
import { applicationTransactionsTableColumns } from '../utils/application-transactions-table-columns'
5+
import { InnerTransaction, Transaction } from '@/features/transactions/models'
6+
import { useCallback } from 'react'
7+
import { getApplicationTransactionsTableSubRows } from '../utils/get-application-transactions-table-sub-rows'
8+
9+
type Props = {
10+
applicationId: ApplicationId
11+
}
12+
13+
export function ApplicationTransactionHistory({ applicationId }: Props) {
14+
// TODO: for the future
15+
// How we handle getSubRows isn't the best practice. Ideally, we should create a new view model, for example, TransactionForApplication
16+
// and then fetchNextPage should return a list of TransactionForApplication
17+
// TransactionForApplication should be similar to Transaction, but the InnerTransactions should be only transactions that are related to the application
18+
// This way, getSubRows simply return the innerTransactions
19+
const fetchNextPage = useFetchNextApplicationTransactionsPage(applicationId)
20+
const getSubRows = useCallback(
21+
(row: Transaction | InnerTransaction) => getApplicationTransactionsTableSubRows(applicationId, row),
22+
[applicationId]
23+
)
24+
25+
return <LazyLoadDataTable columns={applicationTransactionsTableColumns} getSubRows={getSubRows} fetchNextPage={fetchNextPage} />
26+
}

src/features/applications/components/labels.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ export const applicationBoxesLabel = 'Boxes'
1919

2020
export const applicationBoxNameLabel = 'Box Name'
2121
export const applicationBoxValueLabel = 'Box Value'
22+
23+
export const applicationTransactionsLabel = 'Activity'
24+
export const applicationLiveTransactionsTabId = 'live-transactions'
25+
export const applicationLiveTransactionsTabLabel = 'Live Transactions'
26+
export const applicationHistoricalTransactionsTabId = 'historical-transactions'
27+
export const applicationHistoricalTransactionsTabLabel = 'Historical Transactions'
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ApplicationId } from './types'
2+
import { indexer } from '@/features/common/data'
3+
import { TransactionResult, TransactionSearchResults } from '@algorandfoundation/algokit-utils/types/indexer'
4+
import { useMemo } from 'react'
5+
import { JotaiStore } from '@/features/common/data/types'
6+
import { createTransactionsAtom, transactionResultsAtom } from '@/features/transactions/data'
7+
import { atomEffect } from 'jotai-effect'
8+
import { atom, useStore } from 'jotai'
9+
10+
const fetchApplicationTransactionResults = async (applicationID: ApplicationId, pageSize: number, nextPageToken?: string) => {
11+
const results = (await indexer
12+
.searchForTransactions()
13+
.applicationID(applicationID)
14+
.nextToken(nextPageToken ?? '')
15+
.limit(pageSize)
16+
.do()) as TransactionSearchResults
17+
return {
18+
transactionResults: results.transactions,
19+
nextPageToken: results['next-token'],
20+
} as const
21+
}
22+
23+
const createSyncEffect = (transactionResults: TransactionResult[]) => {
24+
return atomEffect((_, set) => {
25+
;(async () => {
26+
try {
27+
set(transactionResultsAtom, (prev) => {
28+
const next = new Map(prev)
29+
transactionResults.forEach((transactionResult) => {
30+
if (!next.has(transactionResult.id)) {
31+
next.set(transactionResult.id, atom(transactionResult))
32+
}
33+
})
34+
return next
35+
})
36+
} catch (e) {
37+
// Ignore any errors as there is nothing to sync
38+
}
39+
})()
40+
})
41+
}
42+
43+
const createApplicationTransactionsAtom = (store: JotaiStore, applicationID: ApplicationId, pageSize: number, nextPageToken?: string) => {
44+
return atom(async (get) => {
45+
const { transactionResults, nextPageToken: newNextPageToken } = await fetchApplicationTransactionResults(
46+
applicationID,
47+
pageSize,
48+
nextPageToken
49+
)
50+
51+
get(createSyncEffect(transactionResults))
52+
53+
const transactions = await get(createTransactionsAtom(store, transactionResults))
54+
55+
return {
56+
rows: transactions,
57+
nextPageToken: newNextPageToken,
58+
}
59+
})
60+
}
61+
62+
export const useFetchNextApplicationTransactionsPage = (applicationID: ApplicationId) => {
63+
const store = useStore()
64+
65+
return useMemo(() => {
66+
return (pageSize: number, nextPageToken?: string) => createApplicationTransactionsAtom(store, applicationID, pageSize, nextPageToken)
67+
}, [store, applicationID])
68+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { InnerTransaction, Transaction, TransactionType } from '@/features/transactions/models'
2+
import { cn } from '@/features/common/utils'
3+
import { ellipseAddress } from '@/utils/ellipse-address'
4+
import { ColumnDef } from '@tanstack/react-table'
5+
import { DisplayAlgo } from '@/features/common/components/display-algo'
6+
import { TransactionLink } from '@/features/transactions/components/transaction-link'
7+
import { asTo } from '@/features/common/mappers/to'
8+
import { InnerTransactionLink } from '@/features/transactions/components/inner-transaction-link'
9+
10+
const indentationWidth = 20
11+
12+
export const applicationTransactionsTableColumns: ColumnDef<Transaction | InnerTransaction>[] = [
13+
{
14+
header: 'Transaction Id',
15+
accessorFn: (transaction) => transaction,
16+
cell: ({ row, getValue }) => {
17+
const transaction = getValue<Transaction | InnerTransaction>()
18+
return (
19+
<div
20+
style={{
21+
marginLeft: `${indentationWidth * row.depth}px`,
22+
}}
23+
>
24+
{'innerId' in transaction ? (
25+
<InnerTransactionLink transactionId={transaction.networkTransactionId} innerTransactionId={transaction.innerId} />
26+
) : (
27+
<TransactionLink transactionId={transaction.id} short={true} />
28+
)}
29+
</div>
30+
)
31+
},
32+
},
33+
{
34+
header: 'Round',
35+
accessorKey: 'confirmedRound',
36+
},
37+
{
38+
accessorKey: 'sender',
39+
header: 'From',
40+
cell: (c) => ellipseAddress(c.getValue<string>()),
41+
},
42+
{
43+
header: 'To',
44+
accessorFn: asTo,
45+
},
46+
{
47+
header: 'Fee',
48+
accessorFn: (transaction) => transaction,
49+
cell: (c) => {
50+
const transaction = c.getValue<Transaction>()
51+
if (transaction.type === TransactionType.ApplicationCall)
52+
return <DisplayAlgo className={cn('justify-center')} amount={transaction.fee} />
53+
},
54+
},
55+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Transaction, InnerTransaction, TransactionType } from '@/features/transactions/models'
2+
import { flattenInnerTransactions } from '@/utils/flatten-inner-transactions'
3+
4+
export const getApplicationTransactionsTableSubRows = (applicationId: number, transaction: Transaction | InnerTransaction) => {
5+
if (transaction.type !== TransactionType.ApplicationCall || transaction.innerTransactions.length === 0) {
6+
return []
7+
}
8+
9+
return transaction.innerTransactions.filter((innerTransaction) => {
10+
const txns = flattenInnerTransactions(innerTransaction)
11+
return txns.some(({ transaction: txn }) => txn.type === TransactionType.ApplicationCall && txn.applicationId === applicationId)
12+
})
13+
}

src/features/assets/components/asset-live-transactions.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,22 @@ import { useCallback } from 'react'
33
import { LiveTransactionsTable } from '@/features/transactions/components/live-transactions-table'
44
import { assetTransactionsTableColumns } from '../utils/asset-transactions-table-columns'
55
import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
6-
import { JotaiStore } from '@/features/common/data/types'
7-
import { createTransactionAtom } from '@/features/transactions/data'
8-
import { atom } from 'jotai'
96
import { getAssetIdsForTransaction } from '@/features/transactions/utils/get-asset-ids-for-transaction'
10-
import { extractTransactionsForAsset } from '../utils/extract-transactions-for-asset'
7+
import { InnerTransaction, Transaction } from '@/features/transactions/models'
8+
import { getAssetTransactionsTableSubRows } from '../utils/get-asset-transactions-table-sub-rows'
119

1210
type Props = {
1311
assetId: AssetId
1412
}
1513

1614
export function AssetLiveTransactions({ assetId }: Props) {
17-
const mapper = useCallback(
18-
(store: JotaiStore, transactionResult: TransactionResult) => {
19-
return atom(async (get) => {
20-
const assetIdsForTransaction = getAssetIdsForTransaction(transactionResult)
21-
if (!assetIdsForTransaction.includes(assetId)) return []
22-
23-
const transaction = await get(createTransactionAtom(store, transactionResult))
24-
return extractTransactionsForAsset(transaction, assetId)
25-
})
15+
const filter = useCallback(
16+
(transactionResult: TransactionResult) => {
17+
const assetIdsForTransaction = getAssetIdsForTransaction(transactionResult)
18+
return assetIdsForTransaction.includes(assetId)
2619
},
2720
[assetId]
2821
)
29-
return <LiveTransactionsTable mapper={mapper} columns={assetTransactionsTableColumns} />
22+
const getSubRows = useCallback((row: Transaction | InnerTransaction) => getAssetTransactionsTableSubRows(assetId, row), [assetId])
23+
return <LiveTransactionsTable filter={filter} getSubRows={getSubRows} columns={assetTransactionsTableColumns} />
3024
}

src/features/assets/components/asset-transaction-history.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import { LazyLoadDataTable } from '@/features/common/components/lazy-load-data-t
22
import { AssetId } from '../data/types'
33
import { useFetchNextAssetTransactionsPage } from '../data/asset-transaction-history'
44
import { assetTransactionsTableColumns } from '../utils/asset-transactions-table-columns'
5+
import { Transaction, InnerTransaction } from '@/features/transactions/models'
6+
import { useCallback } from 'react'
7+
import { getAssetTransactionsTableSubRows } from '../utils/get-asset-transactions-table-sub-rows'
58

69
type Props = {
710
assetId: AssetId
811
}
912

1013
export function AssetTransactionHistory({ assetId }: Props) {
1114
const fetchNextPage = useFetchNextAssetTransactionsPage(assetId)
15+
const getSubRows = useCallback((row: Transaction | InnerTransaction) => getAssetTransactionsTableSubRows(assetId, row), [assetId])
1216

13-
return <LazyLoadDataTable columns={assetTransactionsTableColumns} fetchNextPage={fetchNextPage} />
17+
return <LazyLoadDataTable columns={assetTransactionsTableColumns} getSubRows={getSubRows} fetchNextPage={fetchNextPage} />
1418
}

src/features/assets/data/asset-transaction-history.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { JotaiStore } from '@/features/common/data/types'
66
import { createTransactionsAtom, transactionResultsAtom } from '@/features/transactions/data'
77
import { atomEffect } from 'jotai-effect'
88
import { atom, useStore } from 'jotai'
9-
import { extractTransactionsForAsset } from '../utils/extract-transactions-for-asset'
109

1110
const fetchAssetTransactionResults = async (assetId: AssetId, pageSize: number, nextPageToken?: string) => {
1211
const results = (await indexer
@@ -48,10 +47,9 @@ const createAssetTransactionsAtom = (store: JotaiStore, assetId: AssetId, pageSi
4847
get(createSyncEffect(transactionResults))
4948

5049
const transactions = await get(createTransactionsAtom(store, transactionResults))
51-
const transactionsForAsset = transactions.flatMap((transaction) => extractTransactionsForAsset(transaction, assetId))
5250

5351
return {
54-
rows: transactionsForAsset,
52+
rows: transactions,
5553
nextPageToken: newNextPageToken,
5654
}
5755
})

0 commit comments

Comments
 (0)