Skip to content

Commit

Permalink
feat: transaction group
Browse files Browse the repository at this point in the history
  • Loading branch information
PatrickDinh authored Apr 29, 2024
1 parent 368a2a7 commit ed0acac
Show file tree
Hide file tree
Showing 51 changed files with 2,651 additions and 598 deletions.
17 changes: 11 additions & 6 deletions src/App.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Urls } from './routes/urls'
import { evalTemplates } from './routes/templated-route'
import { TransactionPage, transactionPageTitle } from './features/transactions/pages/transaction-page'
import { ExplorePage, explorePageTitle } from './features/explore/pages/explore-page'
import { GroupPage } from './features/transactions/pages/group-page'
import { GroupPage } from './features/groups/pages/group-page'
import { ErrorPage } from './features/common/pages/error-page'
import { BlockPage, blockPageTitle } from './features/blocks/pages/block-page'
import { InnerTransactionPage } from './features/transactions/pages/inner-transaction-page'
Expand Down Expand Up @@ -53,14 +53,19 @@ export const routes = evalTemplates([
},
],
},
{
template: Urls.Explore.Group.ById,
element: <GroupPage />,
},
{
template: Urls.Explore.Block.ById,
element: <BlockPage />,
errorElement: <ErrorPage title={blockPageTitle} />,
children: [
{
template: Urls.Explore.Block.ById,
element: <BlockPage />,
},
{
template: Urls.Explore.Block.ById.Group.ById,
element: <GroupPage />,
},
],
},
{
template: Urls.Explore.Account.ById,
Expand Down
10 changes: 7 additions & 3 deletions src/features/blocks/components/transactions.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { DisplayAlgo } from '@/features/common/components/display-algo'
import { ellipseAddress } from '@/utils/ellipse-address'
import { ellipseId } from '@/utils/ellipse-id'
import { Transaction, TransactionType } from '@/features/transactions/models'
import { ColumnDef } from '@tanstack/react-table'
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'

type Props = {
transactions: Transaction[]
Expand All @@ -19,7 +19,11 @@ export const columns: ColumnDef<Transaction>[] = [
},
{
header: 'Group ID',
accessorFn: (transaction) => ellipseId(transaction.group),
accessorFn: (transaction) => transaction,
cell: (c) => {
const transaction = c.getValue<Transaction>()
return transaction.group ? <GroupLink round={transaction.confirmedRound} groupId={transaction.group} short={true} /> : undefined
},
},
{
header: 'From',
Expand Down Expand Up @@ -50,7 +54,7 @@ export const columns: ColumnDef<Transaction>[] = [
if (value.type === TransactionType.AssetTransfer) {
return <DisplayAssetAmount amount={value.amount} asset={value.asset} />
}
return <></>
return undefined
},
},
]
Expand Down
49 changes: 41 additions & 8 deletions src/features/blocks/data/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { useMemo } from 'react'
import { loadable } from 'jotai/utils'
import { blockResultsAtom, syncedRoundAtom } from './core'
import { BlockResult, Round } from './types'
import { groupResultsAtom } from '@/features/groups/data/core'
import { GroupId, GroupResult } from '@/features/groups/data/types'

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

const fetchBlockResultAtomBuilder = (round: Round) => {
export const fetchBlockResultAtomBuilder = (round: Round) => {
return atom(async (_get) => {
return await indexer
.lookupBlock(round)
.do()
.then((result) => {
const [transactionIds, groupResults] = ((result.transactions ?? []) as TransactionResult[]).reduce(
(acc, t) => {
acc[0].push(t.id)
if (t.group) {
const group: GroupResult = acc[1].get(t.group) ?? {
id: t.group,
round: result.round as number,
timestamp: new Date(result.timestamp * 1000).toISOString(),
transactionIds: [],
}
group.transactionIds.push(t.id)
acc[1].set(t.group, group)
}
return acc
},
[[], new Map()] as [string[], Map<GroupId, GroupResult>]
)

return [
{
round: result.round as number,
timestamp: new Date(result.timestamp * 1000).toISOString(),
transactionIds: result.transactions?.map((t: TransactionResult) => t.id) ?? [],
transactionIds,
} as BlockResult,
(result.transactions ?? []) as TransactionResult[],
groupResults,
] as const
})
})
}

const getBlockAtomBuilder = (store: JotaiStore, round: Round) => {
const fetchBlockResultAtom = fetchBlockResultAtomBuilder(round)

const syncEffect = atomEffect((get, set) => {
export const syncBlockAtomEffectBuilder = (fetchBlockResultAtom: ReturnType<typeof fetchBlockResultAtomBuilder>) => {
return atomEffect((get, set) => {
;(async () => {
try {
const [blockResult, transactionResults] = await get(fetchBlockResultAtom)
const [blockResult, transactionResults, groupResults] = await get(fetchBlockResultAtom)

if (transactionResults && transactionResults.length > 0) {
if (transactionResults.length > 0) {
set(transactionResultsAtom, (prev) => {
transactionResults.forEach((t) => {
prev.set(t.id, t)
Expand All @@ -54,6 +73,15 @@ const getBlockAtomBuilder = (store: JotaiStore, round: Round) => {
})
}

if (groupResults.size > 0) {
set(groupResultsAtom, (prev) => {
groupResults.forEach((g) => {
prev.set(g.id, g)
})
return prev
})
}

set(blockResultsAtom, (prev) => {
return prev.set(blockResult.round, blockResult)
})
Expand All @@ -62,6 +90,11 @@ const getBlockAtomBuilder = (store: JotaiStore, round: Round) => {
}
})()
})
}

const getBlockAtomBuilder = (store: JotaiStore, round: Round) => {
const fetchBlockResultAtom = fetchBlockResultAtomBuilder(round)
const syncEffect = syncBlockAtomEffectBuilder(fetchBlockResultAtom)

return atom(async (get) => {
const blockResults = store.get(blockResultsAtom)
Expand Down
15 changes: 3 additions & 12 deletions src/features/blocks/mappers/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { Transaction, TransactionSummary, TransactionType } from '@/features/transactions/models'
import { Transaction, TransactionSummary } from '@/features/transactions/models'
import { Block, BlockSummary, CommonBlockProperties } from '../models'
import { BlockResult } from '../data/types'
import { asTransactionsSummary } from '@/features/common/mappers'

const asCommonBlock = (block: BlockResult, transactions: Pick<Transaction, 'type'>[]): CommonBlockProperties => {
return {
round: block.round,
timestamp: block.timestamp,
transactionsSummary: {
count: transactions.length,
countByType: Array.from(
transactions
.reduce((acc, transaction) => {
const count = (acc.get(transaction.type) || 0) + 1
return new Map([...acc, [transaction.type, count]])
}, new Map<TransactionType, number>())
.entries()
),
},
transactionsSummary: asTransactionsSummary(transactions),
}
}

Expand Down
8 changes: 2 additions & 6 deletions src/features/blocks/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { Transaction, TransactionSummary, TransactionType } from '@/features/transactions/models'

export type TransactionsSummary = {
count: number
countByType: [TransactionType, number][]
}
import { TransactionsSummary } from '@/features/common/models'
import { Transaction, TransactionSummary } from '@/features/transactions/models'

export type CommonBlockProperties = {
round: number
Expand Down
16 changes: 16 additions & 0 deletions src/features/common/mappers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Transaction, TransactionType } from '@/features/transactions/models'
import { TransactionsSummary } from '../models'

export const asTransactionsSummary = (transactions: Pick<Transaction, 'type'>[]): TransactionsSummary => {
return {
count: transactions.length,
countByType: Array.from(
transactions
.reduce((acc, transaction) => {
const count = (acc.get(transaction.type) || 0) + 1
return new Map([...acc, [transaction.type, count]])
}, new Map<TransactionType, number>())
.entries()
),
}
}
6 changes: 6 additions & 0 deletions src/features/common/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TransactionType } from '@/features/transactions/models'

export type TransactionsSummary = {
count: number
countByType: [TransactionType, number][]
}
67 changes: 67 additions & 0 deletions src/features/groups/components/group-details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Card, CardContent } from '@/features/common/components/card'
import { Group } from '../models'
import { cn } from '@/features/common/utils'
import { DescriptionList } from '@/features/common/components/description-list'
import { useMemo } from 'react'
import { Badge } from '@/features/common/components/badge'
import { dateFormatter } from '@/utils/format'
import { BlockLink } from '@/features/blocks/components/block-link'
import { GroupVisualTabs } from './group-visual-tabs'

type Props = {
group: Group
}

export const groupIdLabel = 'Group ID'
export const blockLabel = 'Block'
export const transactionsLabel = 'Transactions'
export const timestampLabel = 'Timestamp'

export function GroupDetails({ group }: Props) {
const groupItems = useMemo(
() => [
{
dt: groupIdLabel,
dd: group.id,
},
{
dt: blockLabel,
dd: <BlockLink round={group.round} />,
},
{
dt: transactionsLabel,
dd: (
<>
{group.transactionsSummary.count}
{group.transactionsSummary.countByType.map(([type, count]) => (
<Badge key={type} variant="outline">
{type}={count}
</Badge>
))}
</>
),
},
{
dt: timestampLabel,
dd: dateFormatter.asLongDateTime(new Date(group.timestamp)),
},
],
[group.id, group.round, group.timestamp, group.transactionsSummary.count, group.transactionsSummary.countByType]
)

return (
<div className={cn('space-y-6 pt-7')}>
<Card className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<DescriptionList items={groupItems} />
</CardContent>
</Card>
<Card className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<h1 className={cn('text-2xl text-primary font-bold')}>{transactionsLabel}</h1>
</CardContent>
<GroupVisualTabs group={group} />
</Card>
</div>
)
}
26 changes: 26 additions & 0 deletions src/features/groups/components/group-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Round } from '@/features/blocks/data/types'
import { cn } from '@/features/common/utils'
import { TemplatedNavLink } from '@/features/routing/components/templated-nav-link/templated-nav-link'
import { Urls } from '@/routes/urls'
import { PropsWithChildren } from 'react'
import { GroupId } from '../data/types'
import { ellipseId } from '@/utils/ellipse-id'

type Props = PropsWithChildren<{
round: Round
groupId: GroupId
short?: boolean
className?: string
}>

export function GroupLink({ round, groupId, short = false, className, children }: Props) {
return (
<TemplatedNavLink
className={cn(!children && 'text-primary underline', className)}
urlTemplate={Urls.Explore.Block.ById.Group.ById}
urlParams={{ round: round.toString(), groupId: encodeURIComponent(groupId) }}
>
{children ? children : short ? ellipseId(groupId) : groupId}
</TemplatedNavLink>
)
}
36 changes: 36 additions & 0 deletions src/features/groups/components/group-visual-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { cn } from '@/features/common/utils'
import { OverflowAutoTabsContent, Tabs, TabsList, TabsTrigger } from '@/features/common/components/tabs'
import { Group } from '../models'
import { TransactionsGraph } from '@/features/transactions/components/transactions-graph'
import { TransactionsTable } from '@/features/transactions/components/transactions-table'

type Props = {
group: Group
}

const graphTabId = 'graph'
const tableTabId = 'table'
export const groupVisual = 'View Group'
export const groupVisualGraphLabel = 'Graph'
export const groupVisualTableLabel = 'Table'

export function GroupVisualTabs({ group }: Props) {
return (
<Tabs defaultValue={graphTabId}>
<TabsList aria-label={groupVisual}>
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={graphTabId}>
{groupVisualGraphLabel}
</TabsTrigger>
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={tableTabId}>
{groupVisualTableLabel}
</TabsTrigger>
</TabsList>
<OverflowAutoTabsContent value={graphTabId}>
<TransactionsGraph transactions={group.transactions} />
</OverflowAutoTabsContent>
<OverflowAutoTabsContent value={tableTabId}>
<TransactionsTable transactions={group.transactions} />
</OverflowAutoTabsContent>
</Tabs>
)
}
4 changes: 4 additions & 0 deletions src/features/groups/data/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { atom } from 'jotai'
import { GroupId, GroupResult } from './types'

export const groupResultsAtom = atom<Map<GroupId, GroupResult>>(new Map())
Loading

0 comments on commit ed0acac

Please sign in to comment.