Skip to content

Commit

Permalink
feat: view application transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
PatrickDinh authored May 20, 2024
1 parent 1da3e2a commit 3aafc0d
Show file tree
Hide file tree
Showing 17 changed files with 312 additions and 73 deletions.
35 changes: 35 additions & 0 deletions src/features/applications/components/application-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,22 @@ import {
applicationGlobalStateByteLabel,
applicationGlobalStateLabel,
applicationGlobalStateUintLabel,
applicationHistoricalTransactionsTabId,
applicationHistoricalTransactionsTabLabel,
applicationIdLabel,
applicationLiveTransactionsTabId,
applicationLiveTransactionsTabLabel,
applicationLocalStateByteLabel,
applicationLocalStateUintLabel,
applicationTransactionsLabel,
} from './labels'
import { isDefined } from '@/utils/is-defined'
import { ApplicationProgram } from './application-program'
import { ApplicationGlobalStateTable } from './application-global-state-table'
import { ApplicationBoxes } from './application-boxes'
import { OverflowAutoTabsContent, Tabs, TabsList, TabsTrigger } from '@/features/common/components/tabs'
import { ApplicationLiveTransactions } from './application-live-transactions'
import { ApplicationTransactionHistory } from './application-transaction-history'

type Props = {
application: Application
Expand Down Expand Up @@ -106,6 +114,33 @@ export function ApplicationDetails({ application }: Props) {
<ApplicationBoxes applicationId={application.id} />
</CardContent>
</Card>
<Card aria-label={applicationTransactionsLabel} className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationTransactionsLabel}</h1>
<Tabs defaultValue={applicationLiveTransactionsTabId}>
<TabsList aria-label={applicationTransactionsLabel}>
<TabsTrigger
className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-48')}
value={applicationLiveTransactionsTabId}
>
{applicationLiveTransactionsTabLabel}
</TabsTrigger>
<TabsTrigger
className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-48')}
value={applicationHistoricalTransactionsTabId}
>
{applicationHistoricalTransactionsTabLabel}
</TabsTrigger>
</TabsList>
<OverflowAutoTabsContent value={applicationLiveTransactionsTabId}>
<ApplicationLiveTransactions applicationId={application.id} />
</OverflowAutoTabsContent>
<OverflowAutoTabsContent value={applicationHistoricalTransactionsTabId}>
<ApplicationTransactionHistory applicationId={application.id} />
</OverflowAutoTabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ApplicationId } from '../data/types'
import { useCallback } from 'react'
import { LiveTransactionsTable } from '@/features/transactions/components/live-transactions-table'
import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
import { flattenTransactionResult } from '@/features/transactions/utils/flatten-transaction-result'
import { TransactionType as AlgoSdkTransactionType } from 'algosdk'
import { applicationTransactionsTableColumns } from '../utils/application-transactions-table-columns'
import { Transaction, InnerTransaction } from '@/features/transactions/models'
import { getApplicationTransactionsTableSubRows } from '../utils/get-application-transactions-table-sub-rows'

type Props = {
applicationId: ApplicationId
}

export function ApplicationLiveTransactions({ applicationId }: Props) {
const filter = useCallback(
(transactionResult: TransactionResult) => {
const flattenedTransactionResults = flattenTransactionResult(transactionResult)
return flattenedTransactionResults.some(
(txn) => txn['tx-type'] === AlgoSdkTransactionType.appl && txn['application-transaction']?.['application-id'] === applicationId
)
},
[applicationId]
)

const getSubRows = useCallback(
(row: Transaction | InnerTransaction) => getApplicationTransactionsTableSubRows(applicationId, row),
[applicationId]
)

return <LiveTransactionsTable filter={filter} getSubRows={getSubRows} columns={applicationTransactionsTableColumns} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { LazyLoadDataTable } from '@/features/common/components/lazy-load-data-table'
import { ApplicationId } from '../data/types'
import { useFetchNextApplicationTransactionsPage } from '../data/application-transaction-history'
import { applicationTransactionsTableColumns } from '../utils/application-transactions-table-columns'
import { InnerTransaction, Transaction } from '@/features/transactions/models'
import { useCallback } from 'react'
import { getApplicationTransactionsTableSubRows } from '../utils/get-application-transactions-table-sub-rows'

type Props = {
applicationId: ApplicationId
}

export function ApplicationTransactionHistory({ applicationId }: Props) {
// TODO: for the future
// How we handle getSubRows isn't the best practice. Ideally, we should create a new view model, for example, TransactionForApplication
// and then fetchNextPage should return a list of TransactionForApplication
// TransactionForApplication should be similar to Transaction, but the InnerTransactions should be only transactions that are related to the application
// This way, getSubRows simply return the innerTransactions
const fetchNextPage = useFetchNextApplicationTransactionsPage(applicationId)
const getSubRows = useCallback(
(row: Transaction | InnerTransaction) => getApplicationTransactionsTableSubRows(applicationId, row),
[applicationId]
)

return <LazyLoadDataTable columns={applicationTransactionsTableColumns} getSubRows={getSubRows} fetchNextPage={fetchNextPage} />
}
6 changes: 6 additions & 0 deletions src/features/applications/components/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ export const applicationBoxesLabel = 'Boxes'

export const applicationBoxNameLabel = 'Box Name'
export const applicationBoxValueLabel = 'Box Value'

export const applicationTransactionsLabel = 'Activity'
export const applicationLiveTransactionsTabId = 'live-transactions'
export const applicationLiveTransactionsTabLabel = 'Live Transactions'
export const applicationHistoricalTransactionsTabId = 'historical-transactions'
export const applicationHistoricalTransactionsTabLabel = 'Historical Transactions'
68 changes: 68 additions & 0 deletions src/features/applications/data/application-transaction-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ApplicationId } from './types'
import { indexer } from '@/features/common/data'
import { TransactionResult, TransactionSearchResults } from '@algorandfoundation/algokit-utils/types/indexer'
import { useMemo } from 'react'
import { JotaiStore } from '@/features/common/data/types'
import { createTransactionsAtom, transactionResultsAtom } from '@/features/transactions/data'
import { atomEffect } from 'jotai-effect'
import { atom, useStore } from 'jotai'

const fetchApplicationTransactionResults = async (applicationID: ApplicationId, pageSize: number, nextPageToken?: string) => {
const results = (await indexer
.searchForTransactions()
.applicationID(applicationID)
.nextToken(nextPageToken ?? '')
.limit(pageSize)
.do()) as TransactionSearchResults
return {
transactionResults: results.transactions,
nextPageToken: results['next-token'],
} as const
}

const createSyncEffect = (transactionResults: TransactionResult[]) => {
return atomEffect((_, set) => {
;(async () => {
try {
set(transactionResultsAtom, (prev) => {
const next = new Map(prev)
transactionResults.forEach((transactionResult) => {
if (!next.has(transactionResult.id)) {
next.set(transactionResult.id, atom(transactionResult))
}
})
return next
})
} catch (e) {
// Ignore any errors as there is nothing to sync
}
})()
})
}

const createApplicationTransactionsAtom = (store: JotaiStore, applicationID: ApplicationId, pageSize: number, nextPageToken?: string) => {
return atom(async (get) => {
const { transactionResults, nextPageToken: newNextPageToken } = await fetchApplicationTransactionResults(
applicationID,
pageSize,
nextPageToken
)

get(createSyncEffect(transactionResults))

const transactions = await get(createTransactionsAtom(store, transactionResults))

return {
rows: transactions,
nextPageToken: newNextPageToken,
}
})
}

export const useFetchNextApplicationTransactionsPage = (applicationID: ApplicationId) => {
const store = useStore()

return useMemo(() => {
return (pageSize: number, nextPageToken?: string) => createApplicationTransactionsAtom(store, applicationID, pageSize, nextPageToken)
}, [store, applicationID])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 { TransactionLink } from '@/features/transactions/components/transaction-link'
import { asTo } from '@/features/common/mappers/to'
import { InnerTransactionLink } from '@/features/transactions/components/inner-transaction-link'

const indentationWidth = 20

export const applicationTransactionsTableColumns: ColumnDef<Transaction | InnerTransaction>[] = [
{
header: 'Transaction Id',
accessorFn: (transaction) => transaction,
cell: ({ row, getValue }) => {
const transaction = getValue<Transaction | InnerTransaction>()
return (
<div
style={{
marginLeft: `${indentationWidth * row.depth}px`,
}}
>
{'innerId' in transaction ? (
<InnerTransactionLink transactionId={transaction.networkTransactionId} innerTransactionId={transaction.innerId} />
) : (
<TransactionLink transactionId={transaction.id} short={true} />
)}
</div>
)
},
},
{
header: 'Round',
accessorKey: 'confirmedRound',
},
{
accessorKey: 'sender',
header: 'From',
cell: (c) => ellipseAddress(c.getValue<string>()),
},
{
header: 'To',
accessorFn: asTo,
},
{
header: 'Fee',
accessorFn: (transaction) => transaction,
cell: (c) => {
const transaction = c.getValue<Transaction>()
if (transaction.type === TransactionType.ApplicationCall)
return <DisplayAlgo className={cn('justify-center')} amount={transaction.fee} />
},
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Transaction, InnerTransaction, TransactionType } from '@/features/transactions/models'
import { flattenInnerTransactions } from '@/utils/flatten-inner-transactions'

export const getApplicationTransactionsTableSubRows = (applicationId: number, transaction: Transaction | InnerTransaction) => {
if (transaction.type !== TransactionType.ApplicationCall || transaction.innerTransactions.length === 0) {
return []
}

return transaction.innerTransactions.filter((innerTransaction) => {
const txns = flattenInnerTransactions(innerTransaction)
return txns.some(({ transaction: txn }) => txn.type === TransactionType.ApplicationCall && txn.applicationId === applicationId)
})
}
22 changes: 8 additions & 14 deletions src/features/assets/components/asset-live-transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,22 @@ 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'
import { InnerTransaction, Transaction } from '@/features/transactions/models'
import { getAssetTransactionsTableSubRows } from '../utils/get-asset-transactions-table-sub-rows'

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)
})
const filter = useCallback(
(transactionResult: TransactionResult) => {
const assetIdsForTransaction = getAssetIdsForTransaction(transactionResult)
return assetIdsForTransaction.includes(assetId)
},
[assetId]
)
return <LiveTransactionsTable mapper={mapper} columns={assetTransactionsTableColumns} />
const getSubRows = useCallback((row: Transaction | InnerTransaction) => getAssetTransactionsTableSubRows(assetId, row), [assetId])
return <LiveTransactionsTable filter={filter} getSubRows={getSubRows} columns={assetTransactionsTableColumns} />
}
6 changes: 5 additions & 1 deletion src/features/assets/components/asset-transaction-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { LazyLoadDataTable } from '@/features/common/components/lazy-load-data-t
import { AssetId } from '../data/types'
import { useFetchNextAssetTransactionsPage } from '../data/asset-transaction-history'
import { assetTransactionsTableColumns } from '../utils/asset-transactions-table-columns'
import { Transaction, InnerTransaction } from '@/features/transactions/models'
import { useCallback } from 'react'
import { getAssetTransactionsTableSubRows } from '../utils/get-asset-transactions-table-sub-rows'

type Props = {
assetId: AssetId
}

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

return <LazyLoadDataTable columns={assetTransactionsTableColumns} fetchNextPage={fetchNextPage} />
return <LazyLoadDataTable columns={assetTransactionsTableColumns} getSubRows={getSubRows} fetchNextPage={fetchNextPage} />
}
4 changes: 1 addition & 3 deletions src/features/assets/data/asset-transaction-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ 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 @@ -48,10 +47,9 @@ 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: transactionsForAsset,
rows: transactions,
nextPageToken: newNextPageToken,
}
})
Expand Down
30 changes: 15 additions & 15 deletions src/features/assets/utils/asset-transactions-table-columns.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
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'
import { InnerTransactionLink } from '@/features/transactions/components/inner-transaction-link'

const indentationWidth = 20
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}
cell: ({ row, getValue }) => {
const transaction = getValue<Transaction | InnerTransaction>()
return (
<div
style={{
marginLeft: `${indentationWidth * row.depth}px`,
}}
>
<span>{ellipseId(transaction.id)}</span>
<span>(Inner)</span>
</TransactionLink>
) : (
<TransactionLink transactionId={transaction.id} short={true} />
{'innerId' in transaction ? (
<InnerTransactionLink transactionId={transaction.networkTransactionId} innerTransactionId={transaction.innerId} />
) : (
<TransactionLink transactionId={transaction.id} short={true} />
)}
</div>
)
},
},
Expand All @@ -49,7 +50,6 @@ export const assetTransactionsTableColumns: ColumnDef<Transaction | InnerTransac
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} />
},
Expand Down
Loading

0 comments on commit 3aafc0d

Please sign in to comment.