Skip to content

Commit 68ca8a0

Browse files
authored
chore: refactor transaction mappers + tests
1 parent e1a13eb commit 68ca8a0

17 files changed

+2690
-307
lines changed

src/features/transactions/components/__snapshots__/application-transaction-view-visual.INDQXWQXHF22SO45EZY7V6FFNI6WUD5FHRVDV6NCU6HD424BJGGA.html

Lines changed: 1747 additions & 0 deletions
Large diffs are not rendered by default.

src/features/transactions/components/app-call-transaction-log.tsx renamed to src/features/transactions/components/app-call-transaction-logs.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
import { cn } from '@/features/common/utils'
22
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@radix-ui/react-tabs'
33
import { useMemo } from 'react'
4-
import { Buffer } from 'buffer'
4+
import { base64ToUtf8 } from '@/utils/base64-to-utf8'
55

66
type Props = {
77
logs: string[]
88
}
99

10-
const logLabel = 'View Logs'
10+
export const logsLabel = 'View Logs'
1111
const base64LogTabId = 'base64'
12-
const base64LogTabLabel = 'Base64'
12+
export const base64LogsTabLabel = 'Base64'
1313
const textLogTabId = 'text'
14-
const textLogTabLabel = 'UTF-8'
14+
export const textLogsTabLabel = 'UTF-8'
1515

1616
export function AppCallTransactionLogs({ logs }: Props) {
1717
const texts = useMemo(() => {
18-
return logs.map((log) => Buffer.from(log, 'base64').toString('utf-8'))
18+
return logs.map((log) => base64ToUtf8(log))
1919
}, [logs])
2020

2121
return (
2222
<div className={cn('space-y-2')}>
2323
<h2 className={cn('text-xl font-bold')}>Logs</h2>
2424
<Tabs defaultValue={base64LogTabId}>
25-
<TabsList aria-label={logLabel}>
25+
<TabsList aria-label={logsLabel}>
2626
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={base64LogTabId}>
27-
{base64LogTabLabel}
27+
{base64LogsTabLabel}
2828
</TabsTrigger>
2929
<TabsTrigger className={cn('data-[state=active]:border-primary data-[state=active]:border-b-2 w-32')} value={textLogTabId}>
30-
{textLogTabLabel}
30+
{textLogsTabLabel}
3131
</TabsTrigger>
3232
</TabsList>
3333
<TabsContent value={base64LogTabId} className={cn('border-solid border-2 border-border h-60 p-4')}>

src/features/transactions/components/app-call-transaction.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { TransactionNote } from './transaction-note'
88
import { AppCallTransactionModel, InnerAppCallTransactionModel, SignatureType } from '../models'
99
import { TransactionViewTabs } from './transaction-view-tabs'
1010
import { AppCallTransactionInfo } from './app-call-transaction-info'
11-
import { AppCallTransactionLogs } from './app-call-transaction-log'
11+
import { AppCallTransactionLogs } from './app-call-transaction-logs'
1212

1313
type Props = {
1414
transaction: AppCallTransactionModel | InnerAppCallTransactionModel

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { cn } from '@/features/common/utils'
22
import { Arc2TransactionNote } from '@algorandfoundation/algokit-utils/types/transaction'
33
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@radix-ui/react-tabs'
44
import { useMemo } from 'react'
5-
import { Buffer } from 'buffer'
65
import { DescriptionList } from '@/features/common/components/description-list'
6+
import { base64ToUtf8 } from '@/utils/base64-to-utf8'
77

88
type TransactionNoteProps = {
99
note: string
@@ -52,7 +52,7 @@ const arc2FormatLabels = {
5252

5353
export function TransactionNote({ note }: TransactionNoteProps) {
5454
const [text, json, arc2, activeTabId] = useMemo(() => {
55-
const text = Buffer.from(note, 'base64').toString('utf-8')
55+
const text = base64ToUtf8(note)
5656
const maybeJson = parseJson(text)
5757
const maybeArc2 = parseArc2(text)
5858
const activeTabId = maybeArc2 ? arc2NoteTabId : maybeJson ? jsonNoteTabId : base64NoteTabId

src/features/transactions/components/transaction-view-visual.test.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
33
import { TransactionViewVisual } from './transaction-view-visual'
44
import { executeComponentTest } from '@/tests/test-component'
55
import { render, prettyDOM } from '@/tests/testing-library'
6-
import { asAppCallTransaction, asAssetTransferTransaction, asPaymentTransaction } from '../mappers/transaction-mappers'
6+
import { asAppCallTransaction, asAssetTransferTransaction, asPaymentTransaction } from '../mappers'
77
import { AssetResult, TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
88
import { assetResultMother } from '@/tests/object-mother/asset-result'
99
import { useParams } from 'react-router-dom'
@@ -70,19 +70,32 @@ describe('asset-transfer-transaction-view-visual', () => {
7070
})
7171

7272
describe('application-call-view-visual', () => {
73-
describe.each([{ transactionResult: transactionResultMother['mainnet-KMNBSQ4ZFX252G7S4VYR4ZDZ3RXIET5CNYQVJUO5OXXPMHAMJCCQ']().build() }])(
73+
describe.each([
74+
{
75+
transactionResult: transactionResultMother['mainnet-KMNBSQ4ZFX252G7S4VYR4ZDZ3RXIET5CNYQVJUO5OXXPMHAMJCCQ']().build(),
76+
assetResults: [],
77+
},
78+
{
79+
transactionResult: transactionResultMother['mainnet-INDQXWQXHF22SO45EZY7V6FFNI6WUD5FHRVDV6NCU6HD424BJGGA']().build(),
80+
assetResults: [
81+
assetResultMother['mainnet-31566704']().build(),
82+
assetResultMother['mainnet-386195940']().build(),
83+
assetResultMother['mainnet-408898501']().build(),
84+
],
85+
},
86+
])(
7487
'when rendering transaction $transactionResult.id',
75-
({ transactionResult: transaction }: { transactionResult: TransactionResult }) => {
88+
({ transactionResult, assetResults }: { transactionResult: TransactionResult; assetResults: AssetResult[] }) => {
7689
it('should match snapshot', () => {
77-
vi.mocked(useParams).mockImplementation(() => ({ transactionId: transaction.id }))
90+
vi.mocked(useParams).mockImplementation(() => ({ transactionId: transactionResult.id }))
7891

79-
const model = asAppCallTransaction(transaction, [])
92+
const model = asAppCallTransaction(transactionResult, assetResults)
8093

8194
return executeComponentTest(
8295
() => render(<TransactionViewVisual transaction={model} />),
8396
async (component) => {
8497
expect(prettyDOM(component.container, prettyDomMaxLength, { highlight: false })).toMatchFileSnapshot(
85-
`__snapshots__/application-transaction-view-visual.${transaction.id}.html`
98+
`__snapshots__/application-transaction-view-visual.${transactionResult.id}.html`
8699
)
87100
}
88101
)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { ApplicationOnComplete, AssetResult, TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
2+
import {
3+
AppCallOnComplete,
4+
AppCallTransactionModel,
5+
BaseAppCallTransactionModel,
6+
InnerAppCallTransactionModel,
7+
TransactionType,
8+
} from '../models'
9+
import { invariant } from '@/utils/invariant'
10+
import { IndexerGlobalStateDelta, IndexerLocalStateDelta, asGlobalStateDelta, asLocalStateDelta } from './state-delta-mappers'
11+
import { mapCommonTransactionProperties, asInnerTransactionId } from './transaction-common-properties-mappers'
12+
import { TransactionType as AlgoSdkTransactionType } from 'algosdk'
13+
import { asInnerPaymentTransaction } from './payment-transaction-mappers'
14+
import { asInnerAssetTransferTransactionModel } from './asset-transfer-transaction-mappers'
15+
16+
const mapCommonAppCallTransactionProperties = (
17+
networkTransactionId: string,
18+
transaction: TransactionResult,
19+
assetResults: AssetResult[],
20+
indexPrefix?: string
21+
) => {
22+
invariant(transaction['application-transaction'], 'application-transaction is not set')
23+
24+
return {
25+
...mapCommonTransactionProperties(transaction),
26+
type: TransactionType.ApplicationCall,
27+
applicationId: transaction['application-transaction']['application-id'],
28+
applicationArgs: transaction['application-transaction']['application-args'] ?? [],
29+
applicationAccounts: transaction['application-transaction'].accounts ?? [],
30+
foreignApps: transaction['application-transaction']['foreign-apps'] ?? [],
31+
foreignAssets: transaction['application-transaction']['foreign-assets'] ?? [],
32+
globalStateDeltas: asGlobalStateDelta(transaction['global-state-delta'] as unknown as IndexerGlobalStateDelta[]),
33+
localStateDeltas: asLocalStateDelta(transaction['local-state-delta'] as unknown as IndexerLocalStateDelta[]),
34+
innerTransactions:
35+
transaction['inner-txns']?.map((innerTransaction, index) => {
36+
// Generate a unique id for the inner transaction
37+
const innerId = indexPrefix ? `${indexPrefix}-${index + 1}` : `${index + 1}`
38+
return asInnerTransactionMode(networkTransactionId, innerId, innerTransaction, assetResults)
39+
}) ?? [],
40+
onCompletion: asAppCallOnComplete(transaction['application-transaction']['on-completion']),
41+
action: transaction['application-transaction']['application-id'] ? 'Call' : 'Create',
42+
logs: transaction['logs'] ?? [],
43+
} satisfies BaseAppCallTransactionModel
44+
}
45+
46+
export const asAppCallTransaction = (transaction: TransactionResult, assetResults: AssetResult[]): AppCallTransactionModel => {
47+
const commonProperties = mapCommonAppCallTransactionProperties(transaction.id, transaction, assetResults)
48+
49+
return {
50+
id: transaction.id,
51+
...commonProperties,
52+
}
53+
}
54+
55+
export const asInnerAppCallTransaction = (
56+
networkTransactionId: string,
57+
index: string,
58+
transaction: TransactionResult,
59+
assetResults: AssetResult[]
60+
): InnerAppCallTransactionModel => {
61+
return {
62+
...asInnerTransactionId(networkTransactionId, index),
63+
...mapCommonAppCallTransactionProperties(networkTransactionId, transaction, assetResults, `${index}`),
64+
}
65+
}
66+
67+
const asAppCallOnComplete = (indexerEnum: ApplicationOnComplete): AppCallOnComplete => {
68+
switch (indexerEnum) {
69+
case ApplicationOnComplete.noop:
70+
return AppCallOnComplete.NoOp
71+
case ApplicationOnComplete.optin:
72+
return AppCallOnComplete.OptIn
73+
case ApplicationOnComplete.closeout:
74+
return AppCallOnComplete.CloseOut
75+
case ApplicationOnComplete.clear:
76+
return AppCallOnComplete.ClearState
77+
case ApplicationOnComplete.update:
78+
return AppCallOnComplete.Update
79+
case ApplicationOnComplete.delete:
80+
return AppCallOnComplete.Delete
81+
}
82+
}
83+
84+
const asInnerTransactionMode = (
85+
networkTransactionId: string,
86+
index: string,
87+
transaction: TransactionResult,
88+
assetResults: AssetResult[]
89+
) => {
90+
if (transaction['tx-type'] === AlgoSdkTransactionType.pay) {
91+
return asInnerPaymentTransaction(networkTransactionId, index, transaction)
92+
}
93+
if (transaction['tx-type'] === AlgoSdkTransactionType.axfer) {
94+
invariant(transaction['asset-transfer-transaction'], 'asset-transfer-transaction is not set')
95+
const assetResult = assetResults.find((asset) => asset.index === transaction['asset-transfer-transaction']!['asset-id'])
96+
invariant(assetResult, `Asset index ${transaction['asset-transfer-transaction']!['asset-id']} not found in cache`)
97+
98+
return asInnerAssetTransferTransactionModel(networkTransactionId, index, transaction, assetResult)
99+
}
100+
if (transaction['tx-type'] === AlgoSdkTransactionType.appl) {
101+
return asInnerAppCallTransaction(networkTransactionId, index, transaction, assetResults)
102+
}
103+
104+
// This could be dangerous as we haven't implemented all the transaction types
105+
throw new Error(`Unsupported inner transaction type: ${transaction['tx-type']}`)
106+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { AssetResult, TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
2+
import {
3+
AssetTransferTransactionModel,
4+
AssetTransferTransactionSubType,
5+
BaseAssetTransferTransactionModel,
6+
InnerAssetTransferTransactionModel,
7+
TransactionType,
8+
} from '../models'
9+
import { invariant } from '@/utils/invariant'
10+
import { asAsset } from '@/features/assets/mappers/asset-mappers'
11+
import { ZERO_ADDRESS } from '@/features/common/constants'
12+
import { asInnerTransactionId, mapCommonTransactionProperties } from './transaction-common-properties-mappers'
13+
14+
const mapCommonAssetTransferTransactionProperties = (transaction: TransactionResult, asset: AssetResult) => {
15+
invariant(transaction['asset-transfer-transaction'], 'asset-transfer-transaction is not set')
16+
17+
const subType = () => {
18+
invariant(transaction['asset-transfer-transaction'], 'asset-transfer-transaction is not set')
19+
20+
if (transaction['asset-transfer-transaction']['close-to']) {
21+
return AssetTransferTransactionSubType.OptOut
22+
}
23+
if (
24+
transaction.sender === transaction['asset-transfer-transaction'].receiver &&
25+
transaction['asset-transfer-transaction'].amount === 0
26+
) {
27+
return AssetTransferTransactionSubType.OptIn
28+
}
29+
if (
30+
transaction.sender === asset.params.clawback &&
31+
transaction['asset-transfer-transaction'].sender &&
32+
transaction['asset-transfer-transaction'].sender !== ZERO_ADDRESS
33+
) {
34+
return AssetTransferTransactionSubType.Clawback
35+
}
36+
37+
undefined
38+
}
39+
40+
return {
41+
...mapCommonTransactionProperties(transaction),
42+
type: TransactionType.AssetTransfer,
43+
subType: subType(),
44+
asset: asAsset(asset),
45+
receiver: transaction['asset-transfer-transaction'].receiver,
46+
amount: transaction['asset-transfer-transaction'].amount,
47+
closeRemainder: transaction['asset-transfer-transaction']['close-to']
48+
? {
49+
to: transaction['asset-transfer-transaction']['close-to'],
50+
amount: transaction['asset-transfer-transaction']['close-amount'] ?? 0,
51+
}
52+
: undefined,
53+
clawbackFrom: transaction['asset-transfer-transaction'].sender,
54+
} satisfies BaseAssetTransferTransactionModel
55+
}
56+
57+
export const asAssetTransferTransaction = (transaction: TransactionResult, asset: AssetResult): AssetTransferTransactionModel => {
58+
return {
59+
id: transaction.id,
60+
...mapCommonAssetTransferTransactionProperties(transaction, asset),
61+
}
62+
}
63+
64+
export const asInnerAssetTransferTransactionModel = (
65+
networkTransactionId: string,
66+
index: string,
67+
transaction: TransactionResult,
68+
asset: AssetResult
69+
): InnerAssetTransferTransactionModel => {
70+
return {
71+
...asInnerTransactionId(networkTransactionId, index),
72+
...mapCommonAssetTransferTransactionProperties(transaction, asset),
73+
}
74+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './payment-transaction-mappers'
2+
export * from './placeholder-transaction-mappers'
3+
export * from './asset-transfer-transaction-mappers'
4+
export * from './app-call-transaction-mappers'
5+
export * from './transaction-mappers'
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
2+
import { BasePaymentTransactionModel, InnerPaymentTransactionModel, PaymentTransactionModel, TransactionType } from '../models'
3+
import { invariant } from '@/utils/invariant'
4+
import * as algokit from '@algorandfoundation/algokit-utils'
5+
import { asInnerTransactionId, mapCommonTransactionProperties } from './transaction-common-properties-mappers'
6+
7+
const mapCommonPaymentTransactionProperties = (transaction: TransactionResult) => {
8+
invariant(transaction['payment-transaction'], 'payment-transaction is not set')
9+
10+
return {
11+
...mapCommonTransactionProperties(transaction),
12+
type: TransactionType.Payment,
13+
receiver: transaction['payment-transaction']['receiver'],
14+
amount: algokit.microAlgos(transaction['payment-transaction']['amount']),
15+
closeRemainder: transaction['payment-transaction']['close-remainder-to']
16+
? {
17+
to: transaction['payment-transaction']['close-remainder-to'],
18+
amount: algokit.microAlgos(transaction['payment-transaction']['close-amount'] ?? 0),
19+
}
20+
: undefined,
21+
} satisfies BasePaymentTransactionModel
22+
}
23+
24+
export const asPaymentTransaction = (transaction: TransactionResult): PaymentTransactionModel => {
25+
return {
26+
id: transaction.id,
27+
...mapCommonPaymentTransactionProperties(transaction),
28+
}
29+
}
30+
31+
export const asInnerPaymentTransaction = (
32+
networkTransactionId: string,
33+
index: string,
34+
transaction: TransactionResult
35+
): InnerPaymentTransactionModel => {
36+
return {
37+
...asInnerTransactionId(networkTransactionId, index),
38+
...mapCommonPaymentTransactionProperties(transaction),
39+
}
40+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
2+
import { PaymentTransactionModel, TransactionType } from '../models'
3+
import * as algokit from '@algorandfoundation/algokit-utils'
4+
import { ZERO_ADDRESS } from '@/features/common/constants'
5+
import { transformSignature } from './transaction-common-properties-mappers'
6+
7+
// This creates a placeholder transaction for transactions that we don't support yet
8+
// TODO: Remove this code, once we support all transaction types
9+
export const asPlaceholderTransaction = (transaction: TransactionResult): PaymentTransactionModel => {
10+
return {
11+
id: transaction.id,
12+
type: TransactionType.Payment,
13+
confirmedRound: transaction['confirmed-round']!,
14+
roundTime: transaction['round-time']! * 1000,
15+
group: transaction['group'],
16+
fee: algokit.microAlgos(transaction.fee),
17+
sender: transaction.sender,
18+
receiver: ZERO_ADDRESS,
19+
amount: algokit.microAlgos(3141592),
20+
signature: transformSignature(transaction.signature),
21+
note: transaction.note,
22+
json: '{ "placeholder": true }',
23+
}
24+
}

0 commit comments

Comments
 (0)