Skip to content

Commit 340a3a1

Browse files
authored
feat: display transaction note (#14)
* feat: display transaction notes
1 parent 8f4b8df commit 340a3a1

File tree

5 files changed

+306
-32
lines changed

5 files changed

+306
-32
lines changed

src/features/transactions/components/payment-transaction.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function PaymentTransaction({ transaction, rawTransaction }: PaymentTrans
8585
<TransactionViewTable transaction={transaction} />
8686
</TabsContent>
8787
</Tabs>
88-
<TransactionNote transaction={transaction} />
88+
{transaction.note && <TransactionNote note={transaction.note} />}
8989
<TransactionJson transaction={rawTransaction} />
9090
{transaction.signature?.type === SignatureType.Multi && <Multisig signature={transaction.signature} />}
9191
{transaction.signature?.type === SignatureType.Logic && <Logicsig signature={transaction.signature} />}

src/features/transactions/components/transaction-note.tsx

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,112 @@
11
import { cn } from '@/features/common/utils'
2+
import { Arc2TransactionNote } from '@algorandfoundation/algokit-utils/types/transaction'
23
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@radix-ui/react-tabs'
3-
import { PaymentTransactionModel } from '../models'
4+
import { useMemo } from 'react'
5+
import { Buffer } from 'buffer'
6+
import { DescriptionList } from '@/features/common/components/description-list'
47

5-
export type Props = {
6-
transaction: PaymentTransactionModel
8+
type TransactionNoteProps = {
9+
note: string
710
}
811

9-
export function TransactionNote({ transaction }: Props) {
12+
function parseJson(maybeJson: string) {
13+
try {
14+
const json = JSON.parse(maybeJson)
15+
if (json && typeof json === 'object') {
16+
return json
17+
}
18+
} catch (e) {
19+
// ignore
20+
}
21+
}
22+
23+
// Based on the ARC-2 spec https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md#specification
24+
const arc2Regex = /^([a-zA-Z0-9][a-zA-Z0-9_/@.-]{4,31}):([mjbu]{1})(.*)$/
25+
function parseArc2(maybeArc2: string) {
26+
const result = maybeArc2.match(arc2Regex)
27+
if (result && result.length === 4) {
28+
return {
29+
dAppName: result[1],
30+
format: result[2] as 'm' | 'b' | 'u' | 'j',
31+
data: result[3],
32+
} satisfies Arc2TransactionNote
33+
}
34+
}
35+
36+
const base64NoteTabId = 'base64'
37+
const textNoteTabId = 'text'
38+
const jsonNoteTabId = 'json'
39+
const arc2NoteTabId = 'arc2'
40+
type TabId = typeof base64NoteTabId | typeof textNoteTabId | typeof jsonNoteTabId | typeof arc2NoteTabId
41+
export const noteLabel = 'View Note Details'
42+
export const base64NoteTabLabel = 'Base64'
43+
export const textNoteTabLabel = 'UTF-8'
44+
export const jsonNoteTabLabel = 'JSON'
45+
export const arc2NoteTabLabel = 'ARC-2'
46+
export const arc2FormatLabels = {
47+
m: 'MsgPack',
48+
b: 'bytes',
49+
u: textNoteTabLabel,
50+
j: jsonNoteTabLabel,
51+
}
52+
53+
export function TransactionNote({ note }: TransactionNoteProps) {
54+
const [text, json, arc2, activeTabId] = useMemo(() => {
55+
const text = Buffer.from(note, 'base64').toString('utf-8')
56+
const maybeJson = parseJson(text)
57+
const maybeArc2 = parseArc2(text)
58+
const activeTabId = maybeArc2 ? arc2NoteTabId : maybeJson ? jsonNoteTabId : base64NoteTabId
59+
return [text, maybeJson, maybeArc2, activeTabId satisfies TabId] as const
60+
}, [note])
61+
1062
return (
1163
<div className={cn('space-y-2')}>
1264
<h2 className={cn('text-xl font-bold')}>Note</h2>
13-
<Tabs defaultValue="text">
14-
<TabsList>
15-
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value="text">
16-
Text
65+
<Tabs defaultValue={activeTabId}>
66+
<TabsList aria-label={noteLabel}>
67+
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={base64NoteTabId}>
68+
{base64NoteTabLabel}
1769
</TabsTrigger>
18-
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value="base64">
19-
Base64
20-
</TabsTrigger>
21-
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value="messagePack">
22-
Message pack
70+
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={textNoteTabId}>
71+
{textNoteTabLabel}
2372
</TabsTrigger>
73+
{json && (
74+
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={jsonNoteTabId}>
75+
{jsonNoteTabLabel}
76+
</TabsTrigger>
77+
)}
78+
{arc2 && (
79+
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={arc2NoteTabId}>
80+
{arc2NoteTabLabel}
81+
</TabsTrigger>
82+
)}
2483
</TabsList>
25-
<TabsContent value="text" className={cn('border-solid border-2 border-border h-60 p-4')}>
26-
{transaction.textNote}
27-
</TabsContent>
28-
<TabsContent value="base64" className={cn('border-solid border-2 border-border h-60 p-4')}>
29-
{transaction.base64Note}
84+
<TabsContent value={base64NoteTabId} className={cn('border-solid border-2 border-border h-60 p-4')}>
85+
{note}
3086
</TabsContent>
31-
<TabsContent value="messagePack" className={cn('border-solid border-2 border-border h-60 p-4')}>
32-
{transaction.messagePackNote}
87+
<TabsContent value={textNoteTabId} className={cn('border-solid border-2 border-border h-60 p-4')}>
88+
{text}
3389
</TabsContent>
90+
{json && (
91+
<TabsContent value={jsonNoteTabId} className={cn('border-solid border-2 border-border h-60 p-4')}>
92+
<pre>{JSON.stringify(json, null, 2)}</pre>
93+
</TabsContent>
94+
)}
95+
{arc2 && (
96+
<TabsContent value={arc2NoteTabId} className={cn('border-solid border-2 border-border h-60 p-4')}>
97+
<DescriptionList
98+
items={[
99+
{ dt: 'DApp Name', dd: arc2.dAppName },
100+
{ dt: 'Format', dd: arc2FormatLabels[arc2.format] },
101+
]}
102+
/>
103+
{arc2.format === 'j' && parseJson(arc2.data) ? (
104+
<pre>{JSON.stringify(parseJson(arc2.data), null, 2)}</pre>
105+
) : (
106+
<pre>{arc2.data}</pre>
107+
)}
108+
</TabsContent>
109+
)}
34110
</Tabs>
35111
</div>
36112
)

src/features/transactions/mappers/transaction-mappers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export const asPaymentTransaction = (transaction: TransactionResult): PaymentTra
99
invariant(transaction['round-time'], 'round-time is not set')
1010
invariant(transaction['payment-transaction'], 'payment-transaction is not set')
1111

12-
// TODO: Handle notes
1312
return {
1413
id: transaction.id,
1514
type: TransactionType.Payment,
@@ -24,6 +23,7 @@ export const asPaymentTransaction = (transaction: TransactionResult): PaymentTra
2423
? algokit.microAlgos(transaction['payment-transaction']['close-amount'])
2524
: undefined,
2625
signature: transformSignature(transaction.signature),
26+
note: transaction.note,
2727
} satisfies PaymentTransactionModel
2828
}
2929

src/features/transactions/models/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ type CommonTransactionProperties = {
1010
group?: string
1111
fee: AlgoAmount
1212
sender: Address
13-
14-
base64Note?: string
15-
textNote?: string
16-
messagePackNote?: string
17-
13+
note?: string
1814
transactions?: TransactionModel[]
1915
signature?: SinglesigModel | MultisigModel | LogicsigModel
2016
}

0 commit comments

Comments
 (0)