Skip to content

Commit ed0acac

Browse files
authored
feat: transaction group
1 parent 368a2a7 commit ed0acac

File tree

51 files changed

+2651
-598
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2651
-598
lines changed

src/App.routes.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Urls } from './routes/urls'
44
import { evalTemplates } from './routes/templated-route'
55
import { TransactionPage, transactionPageTitle } from './features/transactions/pages/transaction-page'
66
import { ExplorePage, explorePageTitle } from './features/explore/pages/explore-page'
7-
import { GroupPage } from './features/transactions/pages/group-page'
7+
import { GroupPage } from './features/groups/pages/group-page'
88
import { ErrorPage } from './features/common/pages/error-page'
99
import { BlockPage, blockPageTitle } from './features/blocks/pages/block-page'
1010
import { InnerTransactionPage } from './features/transactions/pages/inner-transaction-page'
@@ -53,14 +53,19 @@ export const routes = evalTemplates([
5353
},
5454
],
5555
},
56-
{
57-
template: Urls.Explore.Group.ById,
58-
element: <GroupPage />,
59-
},
6056
{
6157
template: Urls.Explore.Block.ById,
62-
element: <BlockPage />,
6358
errorElement: <ErrorPage title={blockPageTitle} />,
59+
children: [
60+
{
61+
template: Urls.Explore.Block.ById,
62+
element: <BlockPage />,
63+
},
64+
{
65+
template: Urls.Explore.Block.ById.Group.ById,
66+
element: <GroupPage />,
67+
},
68+
],
6469
},
6570
{
6671
template: Urls.Explore.Account.ById,

src/features/blocks/components/transactions.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { DisplayAlgo } from '@/features/common/components/display-algo'
22
import { ellipseAddress } from '@/utils/ellipse-address'
3-
import { ellipseId } from '@/utils/ellipse-id'
43
import { Transaction, TransactionType } from '@/features/transactions/models'
54
import { ColumnDef } from '@tanstack/react-table'
65
import { DataTable } from '@/features/common/components/data-table'
76
import { TransactionLink } from '@/features/transactions/components/transaction-link'
87
import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount'
8+
import { GroupLink } from '@/features/groups/components/group-link'
99

1010
type Props = {
1111
transactions: Transaction[]
@@ -19,7 +19,11 @@ export const columns: ColumnDef<Transaction>[] = [
1919
},
2020
{
2121
header: 'Group ID',
22-
accessorFn: (transaction) => ellipseId(transaction.group),
22+
accessorFn: (transaction) => transaction,
23+
cell: (c) => {
24+
const transaction = c.getValue<Transaction>()
25+
return transaction.group ? <GroupLink round={transaction.confirmedRound} groupId={transaction.group} short={true} /> : undefined
26+
},
2327
},
2428
{
2529
header: 'From',
@@ -50,7 +54,7 @@ export const columns: ColumnDef<Transaction>[] = [
5054
if (value.type === TransactionType.AssetTransfer) {
5155
return <DisplayAssetAmount amount={value.amount} asset={value.asset} />
5256
}
53-
return <></>
57+
return undefined
5458
},
5559
},
5660
]

src/features/blocks/data/block.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { useMemo } from 'react'
99
import { loadable } from 'jotai/utils'
1010
import { blockResultsAtom, syncedRoundAtom } from './core'
1111
import { BlockResult, Round } from './types'
12+
import { groupResultsAtom } from '@/features/groups/data/core'
13+
import { GroupId, GroupResult } from '@/features/groups/data/types'
1214

1315
const nextRoundAvailableAtomBuilder = (store: JotaiStore, round: Round) => {
1416
// This atom conditionally subscribes to updates on the syncedRoundAtom
@@ -19,33 +21,50 @@ const nextRoundAvailableAtomBuilder = (store: JotaiStore, round: Round) => {
1921
})
2022
}
2123

22-
const fetchBlockResultAtomBuilder = (round: Round) => {
24+
export const fetchBlockResultAtomBuilder = (round: Round) => {
2325
return atom(async (_get) => {
2426
return await indexer
2527
.lookupBlock(round)
2628
.do()
2729
.then((result) => {
30+
const [transactionIds, groupResults] = ((result.transactions ?? []) as TransactionResult[]).reduce(
31+
(acc, t) => {
32+
acc[0].push(t.id)
33+
if (t.group) {
34+
const group: GroupResult = acc[1].get(t.group) ?? {
35+
id: t.group,
36+
round: result.round as number,
37+
timestamp: new Date(result.timestamp * 1000).toISOString(),
38+
transactionIds: [],
39+
}
40+
group.transactionIds.push(t.id)
41+
acc[1].set(t.group, group)
42+
}
43+
return acc
44+
},
45+
[[], new Map()] as [string[], Map<GroupId, GroupResult>]
46+
)
47+
2848
return [
2949
{
3050
round: result.round as number,
3151
timestamp: new Date(result.timestamp * 1000).toISOString(),
32-
transactionIds: result.transactions?.map((t: TransactionResult) => t.id) ?? [],
52+
transactionIds,
3353
} as BlockResult,
3454
(result.transactions ?? []) as TransactionResult[],
55+
groupResults,
3556
] as const
3657
})
3758
})
3859
}
3960

40-
const getBlockAtomBuilder = (store: JotaiStore, round: Round) => {
41-
const fetchBlockResultAtom = fetchBlockResultAtomBuilder(round)
42-
43-
const syncEffect = atomEffect((get, set) => {
61+
export const syncBlockAtomEffectBuilder = (fetchBlockResultAtom: ReturnType<typeof fetchBlockResultAtomBuilder>) => {
62+
return atomEffect((get, set) => {
4463
;(async () => {
4564
try {
46-
const [blockResult, transactionResults] = await get(fetchBlockResultAtom)
65+
const [blockResult, transactionResults, groupResults] = await get(fetchBlockResultAtom)
4766

48-
if (transactionResults && transactionResults.length > 0) {
67+
if (transactionResults.length > 0) {
4968
set(transactionResultsAtom, (prev) => {
5069
transactionResults.forEach((t) => {
5170
prev.set(t.id, t)
@@ -54,6 +73,15 @@ const getBlockAtomBuilder = (store: JotaiStore, round: Round) => {
5473
})
5574
}
5675

76+
if (groupResults.size > 0) {
77+
set(groupResultsAtom, (prev) => {
78+
groupResults.forEach((g) => {
79+
prev.set(g.id, g)
80+
})
81+
return prev
82+
})
83+
}
84+
5785
set(blockResultsAtom, (prev) => {
5886
return prev.set(blockResult.round, blockResult)
5987
})
@@ -62,6 +90,11 @@ const getBlockAtomBuilder = (store: JotaiStore, round: Round) => {
6290
}
6391
})()
6492
})
93+
}
94+
95+
const getBlockAtomBuilder = (store: JotaiStore, round: Round) => {
96+
const fetchBlockResultAtom = fetchBlockResultAtomBuilder(round)
97+
const syncEffect = syncBlockAtomEffectBuilder(fetchBlockResultAtom)
6598

6699
return atom(async (get) => {
67100
const blockResults = store.get(blockResultsAtom)

src/features/blocks/mappers/index.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,13 @@
1-
import { Transaction, TransactionSummary, TransactionType } from '@/features/transactions/models'
1+
import { Transaction, TransactionSummary } from '@/features/transactions/models'
22
import { Block, BlockSummary, CommonBlockProperties } from '../models'
33
import { BlockResult } from '../data/types'
4+
import { asTransactionsSummary } from '@/features/common/mappers'
45

56
const asCommonBlock = (block: BlockResult, transactions: Pick<Transaction, 'type'>[]): CommonBlockProperties => {
67
return {
78
round: block.round,
89
timestamp: block.timestamp,
9-
transactionsSummary: {
10-
count: transactions.length,
11-
countByType: Array.from(
12-
transactions
13-
.reduce((acc, transaction) => {
14-
const count = (acc.get(transaction.type) || 0) + 1
15-
return new Map([...acc, [transaction.type, count]])
16-
}, new Map<TransactionType, number>())
17-
.entries()
18-
),
19-
},
10+
transactionsSummary: asTransactionsSummary(transactions),
2011
}
2112
}
2213

src/features/blocks/models/index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import { Transaction, TransactionSummary, TransactionType } from '@/features/transactions/models'
2-
3-
export type TransactionsSummary = {
4-
count: number
5-
countByType: [TransactionType, number][]
6-
}
1+
import { TransactionsSummary } from '@/features/common/models'
2+
import { Transaction, TransactionSummary } from '@/features/transactions/models'
73

84
export type CommonBlockProperties = {
95
round: number
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Transaction, TransactionType } from '@/features/transactions/models'
2+
import { TransactionsSummary } from '../models'
3+
4+
export const asTransactionsSummary = (transactions: Pick<Transaction, 'type'>[]): TransactionsSummary => {
5+
return {
6+
count: transactions.length,
7+
countByType: Array.from(
8+
transactions
9+
.reduce((acc, transaction) => {
10+
const count = (acc.get(transaction.type) || 0) + 1
11+
return new Map([...acc, [transaction.type, count]])
12+
}, new Map<TransactionType, number>())
13+
.entries()
14+
),
15+
}
16+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { TransactionType } from '@/features/transactions/models'
2+
3+
export type TransactionsSummary = {
4+
count: number
5+
countByType: [TransactionType, number][]
6+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Card, CardContent } from '@/features/common/components/card'
2+
import { Group } from '../models'
3+
import { cn } from '@/features/common/utils'
4+
import { DescriptionList } from '@/features/common/components/description-list'
5+
import { useMemo } from 'react'
6+
import { Badge } from '@/features/common/components/badge'
7+
import { dateFormatter } from '@/utils/format'
8+
import { BlockLink } from '@/features/blocks/components/block-link'
9+
import { GroupVisualTabs } from './group-visual-tabs'
10+
11+
type Props = {
12+
group: Group
13+
}
14+
15+
export const groupIdLabel = 'Group ID'
16+
export const blockLabel = 'Block'
17+
export const transactionsLabel = 'Transactions'
18+
export const timestampLabel = 'Timestamp'
19+
20+
export function GroupDetails({ group }: Props) {
21+
const groupItems = useMemo(
22+
() => [
23+
{
24+
dt: groupIdLabel,
25+
dd: group.id,
26+
},
27+
{
28+
dt: blockLabel,
29+
dd: <BlockLink round={group.round} />,
30+
},
31+
{
32+
dt: transactionsLabel,
33+
dd: (
34+
<>
35+
{group.transactionsSummary.count}
36+
{group.transactionsSummary.countByType.map(([type, count]) => (
37+
<Badge key={type} variant="outline">
38+
{type}={count}
39+
</Badge>
40+
))}
41+
</>
42+
),
43+
},
44+
{
45+
dt: timestampLabel,
46+
dd: dateFormatter.asLongDateTime(new Date(group.timestamp)),
47+
},
48+
],
49+
[group.id, group.round, group.timestamp, group.transactionsSummary.count, group.transactionsSummary.countByType]
50+
)
51+
52+
return (
53+
<div className={cn('space-y-6 pt-7')}>
54+
<Card className={cn('p-4')}>
55+
<CardContent className={cn('text-sm space-y-2')}>
56+
<DescriptionList items={groupItems} />
57+
</CardContent>
58+
</Card>
59+
<Card className={cn('p-4')}>
60+
<CardContent className={cn('text-sm space-y-2')}>
61+
<h1 className={cn('text-2xl text-primary font-bold')}>{transactionsLabel}</h1>
62+
</CardContent>
63+
<GroupVisualTabs group={group} />
64+
</Card>
65+
</div>
66+
)
67+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Round } from '@/features/blocks/data/types'
2+
import { cn } from '@/features/common/utils'
3+
import { TemplatedNavLink } from '@/features/routing/components/templated-nav-link/templated-nav-link'
4+
import { Urls } from '@/routes/urls'
5+
import { PropsWithChildren } from 'react'
6+
import { GroupId } from '../data/types'
7+
import { ellipseId } from '@/utils/ellipse-id'
8+
9+
type Props = PropsWithChildren<{
10+
round: Round
11+
groupId: GroupId
12+
short?: boolean
13+
className?: string
14+
}>
15+
16+
export function GroupLink({ round, groupId, short = false, className, children }: Props) {
17+
return (
18+
<TemplatedNavLink
19+
className={cn(!children && 'text-primary underline', className)}
20+
urlTemplate={Urls.Explore.Block.ById.Group.ById}
21+
urlParams={{ round: round.toString(), groupId: encodeURIComponent(groupId) }}
22+
>
23+
{children ? children : short ? ellipseId(groupId) : groupId}
24+
</TemplatedNavLink>
25+
)
26+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { cn } from '@/features/common/utils'
2+
import { OverflowAutoTabsContent, Tabs, TabsList, TabsTrigger } from '@/features/common/components/tabs'
3+
import { Group } from '../models'
4+
import { TransactionsGraph } from '@/features/transactions/components/transactions-graph'
5+
import { TransactionsTable } from '@/features/transactions/components/transactions-table'
6+
7+
type Props = {
8+
group: Group
9+
}
10+
11+
const graphTabId = 'graph'
12+
const tableTabId = 'table'
13+
export const groupVisual = 'View Group'
14+
export const groupVisualGraphLabel = 'Graph'
15+
export const groupVisualTableLabel = 'Table'
16+
17+
export function GroupVisualTabs({ group }: Props) {
18+
return (
19+
<Tabs defaultValue={graphTabId}>
20+
<TabsList aria-label={groupVisual}>
21+
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={graphTabId}>
22+
{groupVisualGraphLabel}
23+
</TabsTrigger>
24+
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={tableTabId}>
25+
{groupVisualTableLabel}
26+
</TabsTrigger>
27+
</TabsList>
28+
<OverflowAutoTabsContent value={graphTabId}>
29+
<TransactionsGraph transactions={group.transactions} />
30+
</OverflowAutoTabsContent>
31+
<OverflowAutoTabsContent value={tableTabId}>
32+
<TransactionsTable transactions={group.transactions} />
33+
</OverflowAutoTabsContent>
34+
</Tabs>
35+
)
36+
}

0 commit comments

Comments
 (0)