Skip to content

Commit 6305ace

Browse files
authored
feat: account historical transactions (#51)
* feat: add account historical transactions * fix: circular dependency issue * refactor: account info labels * fix: filtering the flattened transactions
1 parent beac938 commit 6305ace

File tree

10 files changed

+244
-41
lines changed

10 files changed

+244
-41
lines changed

src/features/accounts/components/account-activity-tabs.tsx

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,22 @@ import { cn } from '@/features/common/utils'
33
import { useMemo } from 'react'
44
import { AccountAssetHeld } from './account-assets-held'
55
import { Account } from '../models'
6-
7-
const accountLiveTransactionsTabId = 'live-transactions'
8-
const accountHistoricalTransactionsTabId = 'historical-transactions'
9-
const accountHeldAssetsTabId = 'held-assets'
10-
const accountCreatedAssetsTabId = 'created-assets'
11-
const accountCreatedApplicationsTabId = 'created-applications'
12-
const accountOptedApplicationsTabId = 'opted-applications'
13-
14-
export const accountDetailsLabel = 'View account Details'
15-
export const accountLiveTransactionsTabLabel = 'Live Transactions'
16-
export const accountHistoricalTransactionsTabLabel = 'Historical Transactions'
17-
export const accountHeldAssetsTabLabel = 'Held Assets'
18-
export const accountCreatedAssetsTabLabel = 'Created Assets'
19-
export const accountCreatedApplicationsTabLabel = 'Created Applications'
20-
export const accountOptedApplicationsTabLabel = 'Opted Applications'
6+
import { AccountTransactionHistory } from './account-transaction-history'
7+
import {
8+
accountActivityLabel,
9+
accountLiveTransactionsTabId,
10+
accountCreatedApplicationsTabId,
11+
accountCreatedAssetsTabId,
12+
accountHeldAssetsTabId,
13+
accountHistoricalTransactionsTabId,
14+
accountOptedApplicationsTabId,
15+
accountLiveTransactionsTabLabel,
16+
accountHistoricalTransactionsTabLabel,
17+
accountHeldAssetsTabLabel,
18+
accountCreatedAssetsTabLabel,
19+
accountCreatedApplicationsTabLabel,
20+
accountOptedApplicationsTabLabel,
21+
} from './labels'
2122

2223
type Props = {
2324
account: Account
@@ -34,7 +35,7 @@ export function AccountActivityTabs({ account }: Props) {
3435
{
3536
id: accountHistoricalTransactionsTabId,
3637
label: accountHistoricalTransactionsTabLabel,
37-
children: '',
38+
children: <AccountTransactionHistory address={account.address} />,
3839
},
3940
{
4041
id: accountHeldAssetsTabId,
@@ -61,7 +62,7 @@ export function AccountActivityTabs({ account }: Props) {
6162
)
6263
return (
6364
<Tabs defaultValue={accountLiveTransactionsTabId}>
64-
<TabsList aria-label={accountDetailsLabel}>
65+
<TabsList aria-label={accountActivityLabel}>
6566
{tabs.map((tab) => (
6667
<TabsTrigger key={tab.id} className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-44')} value={tab.id}>
6768
{tab.label}

src/features/accounts/components/account-details.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,19 @@ import { Account } from '../models'
33
import { cn } from '@/features/common/utils'
44
import { AccountActivityTabs } from './account-activity-tabs'
55
import { AccountInfo } from './account-info'
6+
import { accountActivityLabel, accountJsonLabel } from './labels'
67

78
type Props = {
89
account: Account
910
}
1011

11-
export const activityLabel = 'Activity'
12-
export const accountJsonLabel = 'Acount JSON'
13-
1412
export function AccountDetails({ account }: Props) {
1513
return (
1614
<div className={cn('space-y-6 pt-7')}>
1715
<AccountInfo account={account} />
18-
<Card aria-label={activityLabel} className={cn('p-4')}>
16+
<Card className={cn('p-4')}>
1917
<CardContent className={cn('text-sm space-y-2')}>
20-
<h1 className={cn('text-2xl text-primary font-bold')}>{activityLabel}</h1>
18+
<h1 className={cn('text-2xl text-primary font-bold')}>{accountActivityLabel}</h1>
2119
<div className={cn('border-solid border-2 border-border grid')}>
2220
<AccountActivityTabs account={account} />
2321
</div>

src/features/accounts/components/account-info.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import { DescriptionList } from '@/features/common/components/description-list'
55
import { cn } from '@/features/common/utils'
66
import { DisplayAlgo } from '@/features/common/components/display-algo'
77
import { AccountLink } from './account-link'
8-
9-
export const accountInformationLabel = 'Account Information'
10-
export const accountAddressLabel = 'Address'
11-
export const accountBalanceLabel = 'Balance'
12-
export const accountMinBalanceLabel = 'Min Balance'
13-
export const accountAssetsHeldLabel = 'Holding assets'
14-
export const accountAssetsCreatedLabel = 'Created assets'
15-
export const accountAssetsOptedInLabel = 'Opted assets'
16-
export const accountApplicationsCreatedLabel = 'Created applications'
17-
export const accountApplicationsOptedInLabel = 'Opted applications'
18-
export const accountRekeyedToLabel = 'Rekeyed to'
8+
import {
9+
accountAddressLabel,
10+
accountApplicationsCreatedLabel,
11+
accountApplicationsOptedInLabel,
12+
accountAssetsCreatedLabel,
13+
accountAssetsHeldLabel,
14+
accountAssetsOptedInLabel,
15+
accountBalanceLabel,
16+
accountInformationLabel,
17+
accountMinBalanceLabel,
18+
accountRekeyedToLabel,
19+
} from './labels'
1920

2021
export function AccountInfo({ account }: { account: Account }) {
2122
const accountInfoItems = useMemo(() => {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { LazyLoadDataTable } from '@/features/common/components/lazy-load-data-table'
2+
import { Address } from '../data/types'
3+
import { useFetchNextAccountTransactionPage } from '../data/account-transaction-history'
4+
import { accountTransactionsTableColumns } from './account-transaction-table-columns'
5+
6+
type Props = {
7+
address: Address
8+
}
9+
10+
export function AccountTransactionHistory({ address }: Props) {
11+
const fetchNextPage = useFetchNextAccountTransactionPage(address)
12+
13+
return <LazyLoadDataTable columns={accountTransactionsTableColumns} fetchNextPage={fetchNextPage} />
14+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 { DisplayAssetAmount } from '@/features/common/components/display-asset-amount'
7+
import { TransactionLink } from '@/features/transactions/components/transaction-link'
8+
import { ellipseId } from '@/utils/ellipse-id'
9+
import { asTo } from '@/features/common/mappers/to'
10+
11+
export const accountTransactionsTableColumns: ColumnDef<Transaction | InnerTransaction>[] = [
12+
{
13+
header: 'Transaction Id',
14+
accessorFn: (transaction) => transaction,
15+
cell: (c) => {
16+
const transaction = c.getValue<Transaction | InnerTransaction>()
17+
return 'innerId' in transaction ? (
18+
<TransactionLink
19+
className={cn('text-primary underline cursor-pointer grid gap-2')}
20+
transactionId={transaction.networkTransactionId}
21+
>
22+
<span>{ellipseId(transaction.id)}</span>
23+
<span>(Inner)</span>
24+
</TransactionLink>
25+
) : (
26+
<TransactionLink transactionId={transaction.id} short={true} />
27+
)
28+
},
29+
},
30+
{
31+
header: 'Round',
32+
accessorKey: 'confirmedRound',
33+
},
34+
{
35+
accessorKey: 'sender',
36+
header: 'From',
37+
cell: (c) => ellipseAddress(c.getValue<string>()),
38+
},
39+
{
40+
header: 'To',
41+
accessorFn: asTo,
42+
},
43+
{
44+
accessorKey: 'type',
45+
header: 'Type',
46+
},
47+
{
48+
header: 'Amount',
49+
accessorFn: (transaction) => transaction,
50+
cell: (c) => {
51+
const transaction = c.getValue<Transaction>()
52+
if (transaction.type === TransactionType.Payment) return <DisplayAlgo className={cn('justify-center')} amount={transaction.amount} />
53+
if (transaction.type === TransactionType.AssetTransfer)
54+
return <DisplayAssetAmount amount={transaction.amount} asset={transaction.asset} />
55+
},
56+
},
57+
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export const accountJsonLabel = 'Acount JSON'
2+
export const accountActivityLabel = 'Activity'
3+
4+
export const accountLiveTransactionsTabLabel = 'Live Transactions'
5+
export const accountHistoricalTransactionsTabLabel = 'Historical Transactions'
6+
export const accountHeldAssetsTabLabel = 'Held Assets'
7+
export const accountCreatedAssetsTabLabel = 'Created Assets'
8+
export const accountCreatedApplicationsTabLabel = 'Created Applications'
9+
export const accountOptedApplicationsTabLabel = 'Opted Applications'
10+
11+
export const accountLiveTransactionsTabId = 'live-transactions'
12+
export const accountHistoricalTransactionsTabId = 'historical-transactions'
13+
export const accountHeldAssetsTabId = 'held-assets'
14+
export const accountCreatedAssetsTabId = 'created-assets'
15+
export const accountCreatedApplicationsTabId = 'created-applications'
16+
export const accountOptedApplicationsTabId = 'opted-applications'
17+
18+
export const accountInformationLabel = 'Account Information'
19+
export const accountAddressLabel = 'Address'
20+
export const accountBalanceLabel = 'Balance'
21+
export const accountMinBalanceLabel = 'Min Balance'
22+
export const accountAssetsHeldLabel = 'Held assets'
23+
export const accountAssetsCreatedLabel = 'Created assets'
24+
export const accountAssetsOptedInLabel = 'Opted assets'
25+
export const accountApplicationsCreatedLabel = 'Created applications'
26+
export const accountApplicationsOptedInLabel = 'Opted applications'
27+
export const accountRekeyedToLabel = 'Rekeyed to'
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Address } from '../data/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+
import { extractTransactionsForAccount } from '../utils/extract-transaction-for-account'
10+
11+
const fetchAccountTransactionResults = async (address: Address, pageSize: number, nextPageToken?: string) => {
12+
const results = (await indexer
13+
.searchForTransactions()
14+
.address(address)
15+
.nextToken(nextPageToken ?? '')
16+
.limit(pageSize)
17+
.do()) as TransactionSearchResults
18+
return {
19+
transactionResults: results.transactions,
20+
nextPageToken: results['next-token'],
21+
} as const
22+
}
23+
24+
const createSyncEffect = (transactionResults: TransactionResult[]) => {
25+
return atomEffect((_, set) => {
26+
;(async () => {
27+
try {
28+
set(transactionResultsAtom, (prev) => {
29+
const next = new Map(prev)
30+
transactionResults.forEach((transactionResult) => {
31+
if (!next.has(transactionResult.id)) {
32+
next.set(transactionResult.id, atom(transactionResult))
33+
}
34+
})
35+
return next
36+
})
37+
} catch (e) {
38+
// Ignore any errors as there is nothing to sync
39+
}
40+
})()
41+
})
42+
}
43+
44+
const creatAccountTransactionAtom = (store: JotaiStore, address: Address, pageSize: number, nextPageToken?: string) => {
45+
return atom(async (get) => {
46+
const { transactionResults, nextPageToken: newNextPageToken } = await fetchAccountTransactionResults(address, pageSize, nextPageToken)
47+
48+
get(createSyncEffect(transactionResults))
49+
50+
const transactions = await get(createTransactionsAtom(store, transactionResults))
51+
const transactionsForAccount = transactions.flatMap((transaction) => extractTransactionsForAccount(transaction, address))
52+
return {
53+
rows: transactionsForAccount,
54+
nextPageToken: newNextPageToken,
55+
}
56+
})
57+
}
58+
59+
export const useFetchNextAccountTransactionPage = (address: Address) => {
60+
const store = useStore()
61+
62+
return useMemo(() => {
63+
return (pageSize: number, nextPageToken?: string) => creatAccountTransactionAtom(store, address, pageSize, nextPageToken)
64+
}, [store, address])
65+
}

src/features/accounts/pages/account-page.test.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { AccountPage, accountFailedToLoadMessage } from './account-page'
66
import { algod } from '@/features/common/data'
77
import { accountResultMother } from '@/tests/object-mother/account-result'
88
import { atom, createStore } from 'jotai'
9+
import { descriptionListAssertion } from '@/tests/assertions/description-list-assertion'
10+
import { accountResultsAtom } from '../data'
911
import {
1012
accountAddressLabel,
1113
accountBalanceLabel,
@@ -16,10 +18,9 @@ import {
1618
accountMinBalanceLabel,
1719
accountApplicationsOptedInLabel,
1820
accountAssetsOptedInLabel,
19-
} from '../components/account-info'
20-
import { descriptionListAssertion } from '@/tests/assertions/description-list-assertion'
21-
import { accountResultsAtom } from '../data'
22-
import { accountJsonLabel } from '../components/account-details'
21+
accountActivityLabel,
22+
accountJsonLabel,
23+
} from '../components/labels'
2324

2425
describe('account-page', () => {
2526
describe('when rendering an account using a invalid address', () => {
@@ -78,6 +79,9 @@ describe('account-page', () => {
7879
{ term: accountApplicationsOptedInLabel, description: '2' },
7980
],
8081
})
82+
const activityTabList = component.getByRole('tablist', { name: accountActivityLabel })
83+
expect(activityTabList).toBeTruthy()
84+
expect(activityTabList.children.length).toBe(6)
8185
})
8286
}
8387
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Transaction, TransactionType } from '@/features/transactions/models'
2+
import { flattenInnerTransactions } from '@/utils/flatten-inner-transactions'
3+
import { Address } from '../data/types'
4+
5+
export const extractTransactionsForAccount = (transaction: Transaction, address: Address) => {
6+
const flattenedTransactions = flattenInnerTransactions(transaction)
7+
const results = []
8+
9+
for (const { transaction } of flattenedTransactions) {
10+
if (flattenedTransactions.length === 1) {
11+
results.push(transaction)
12+
} else {
13+
if (transaction.sender === address) {
14+
results.push(transaction)
15+
} else if (
16+
(transaction.type === TransactionType.Payment || transaction.type === TransactionType.AssetTransfer) &&
17+
(transaction.receiver === address || transaction.closeRemainder?.to === address)
18+
) {
19+
results.push(transaction)
20+
} else if (
21+
transaction.type === TransactionType.AssetConfig &&
22+
(transaction.manager === address ||
23+
transaction.clawback === address ||
24+
transaction.reserve === address ||
25+
transaction.freeze === address)
26+
) {
27+
results.push(transaction)
28+
} else if (transaction.type === TransactionType.AssetTransfer && transaction.clawbackFrom === address) {
29+
results.push(transaction)
30+
} else if (transaction.type === TransactionType.AssetFreeze && transaction.address === address) {
31+
results.push(transaction)
32+
}
33+
}
34+
}
35+
return results
36+
}

src/features/transactions/models/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,11 @@ export type BaseAssetConfigTransaction = CommonTransactionProperties & {
173173
total?: number | bigint
174174
decimals?: number | bigint
175175
unitName?: string
176-
clawback?: string
176+
clawback?: Address
177177
subType: AssetConfigTransactionSubType
178-
manager?: string
179-
reserve?: string
180-
freeze?: string
178+
manager?: Address
179+
reserve?: Address
180+
freeze?: Address
181181
defaultFrozen?: boolean
182182
}
183183

0 commit comments

Comments
 (0)