Skip to content

Commit 374605f

Browse files
authored
fix: handle a deleted asset (#21)
* fix: handle a deleted asset
1 parent 26ae636 commit 374605f

File tree

7 files changed

+132
-14
lines changed

7 files changed

+132
-14
lines changed

src/features/assets/data/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ import { loadable } from 'jotai/utils'
66
import { JotaiStore } from '@/features/common/data/types'
77
import { indexer } from '@/features/common/data'
88
import { AssetIndex } from './types'
9+
import { ZERO_ADDRESS } from '@/features/common/constants'
10+
import { asError, is404 } from '@/utils/error'
11+
12+
const deletedAssetBuilder = (assetIndex: AssetIndex) => {
13+
return {
14+
index: assetIndex,
15+
deleted: true,
16+
params: {
17+
creator: ZERO_ADDRESS,
18+
decimals: 0,
19+
total: 0,
20+
name: 'DELETED',
21+
'unit-name': 'DELETED',
22+
},
23+
} as AssetResult
24+
}
925

1026
// TODO: Size should be capped at some limit, so memory usage doesn't grow indefinitely
1127
export const assetsAtom = atom<Map<AssetIndex, AssetResult>>(new Map())
@@ -38,6 +54,12 @@ export const fetchAssetAtomBuilder = (store: JotaiStore, assetIndex: AssetIndex)
3854
.then((result) => {
3955
return (result as AssetLookupResult).asset
4056
})
57+
.catch((e: unknown) => {
58+
if (is404(asError(e))) {
59+
return deletedAssetBuilder(assetIndex)
60+
}
61+
throw e
62+
})
4163
})
4264
return assetAtom
4365
}

src/features/blocks/pages/block-page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import { useLoadableBlockModel } from '../data'
55
import { RenderLoadable } from '@/features/common/components/render-loadable'
66
import { cn } from '@/features/common/utils'
77
import { Block } from '../components/block'
8+
import { is404 } from '@/utils/error'
89

910
const validRoundRegex = /^\d+$/
1011
const isValidRound = (round: string) => round.match(validRoundRegex)
1112

1213
const transformError = (e: Error) => {
13-
if ('status' in e && e.status === 404) {
14+
if (is404(e)) {
1415
return new Error(blockNotFoundMessage)
1516
}
1617

src/features/transactions/pages/transaction-page.test.tsx

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { transactionsAtom } from '../data'
1515
import { lookupTransactionById } from '@algorandfoundation/algokit-utils'
1616
import { HttpError } from '@/tests/errors'
1717
import { base64LogicsigTabLabel, tealLogicsigTabLabel, logicsigLabel } from '../components/logicsig'
18-
import { algod } from '@/features/common/data'
18+
import { algod, indexer } from '@/features/common/data'
1919
import {
2020
tableTransactionDetailsTabLabel,
2121
transactionDetailsLabel,
@@ -427,7 +427,7 @@ describe('transaction-page', () => {
427427
})
428428
})
429429

430-
describe('when rendering a asset transfer transaction', () => {
430+
describe('when rendering an asset transfer transaction', () => {
431431
const transaction = transactionResultMother['mainnet-V7GQPE5TDMB4BIW2GCTPCBMXYMCF3HQGLYOYHGWP256GQHN5QAXQ']().build()
432432
const asset = assetResultMother['mainnet-140479105']().build()
433433

@@ -490,7 +490,7 @@ describe('transaction-page', () => {
490490
})
491491
})
492492

493-
describe('when rendering a asset opt-in transaction', () => {
493+
describe('when rendering an asset opt-in transaction', () => {
494494
const transaction = transactionResultMother['mainnet-563MNGEL2OF4IBA7CFLIJNMBETT5QNKZURSLIONJBTJFALGYOAUA']().build()
495495
const asset = assetResultMother['mainnet-312769']().build()
496496

@@ -515,7 +515,7 @@ describe('transaction-page', () => {
515515
})
516516
})
517517

518-
describe('when rendering a asset clawback transaction', () => {
518+
describe('when rendering an asset clawback transaction', () => {
519519
const transaction = transactionResultMother['testnet-VIXTUMAPT7NR4RB2WVOGMETW4QY43KIDA3HWDWWXS3UEDKGTEECQ']().build()
520520
const asset = assetResultMother['testnet-642327435']().build()
521521

@@ -549,4 +549,64 @@ describe('transaction-page', () => {
549549
)
550550
})
551551
})
552+
553+
describe('when rendering an asset transfer transaction for a deleted asset', () => {
554+
const transaction = transactionResultMother['mainnet-UFYPQDLWCVK3L5XVVHE7WBQWTW4YMHHKZSDIWXXV2AGCS646HTQA']().build()
555+
// const asset = assetResultMother['mainnet-140479105']().build()
556+
557+
it('should be rendered with the correct data', () => {
558+
vi.mocked(useParams).mockImplementation(() => ({ transactionId: transaction.id }))
559+
vi.mocked(indexer.lookupAssetByID(0).do).mockImplementation(() => Promise.reject(new HttpError('boom', 404)))
560+
const myStore = createStore()
561+
myStore.set(transactionsAtom, new Map([[transaction.id, transaction]]))
562+
563+
return executeComponentTest(
564+
() => {
565+
return render(<TransactionPage />, undefined, myStore)
566+
},
567+
async (component, user) => {
568+
// waitFor the loading state to be finished
569+
await waitFor(() => expect(getByDescriptionTerm(component.container, transactionIdLabel).textContent).toBe(transaction.id))
570+
const transactionTypeDescription = getByDescriptionTerm(component.container, transactionTypeLabel).textContent
571+
expect(transactionTypeDescription).toContain('Asset Transfer')
572+
expect(getByDescriptionTerm(component.container, transactionTimestampLabel).textContent).toBe('Wed, 17 April 2024 05:39:26')
573+
expect(getByDescriptionTerm(component.container, transactionBlockLabel).textContent).toBe('38008738')
574+
expect(getByDescriptionTerm(component.container, transactionGroupLabel).textContent).toBe(
575+
'XeNQmhxvtoWpue/7SAk6RNfuu/8Fp8tw8Nfn+HnIz00='
576+
)
577+
578+
expect(getByDescriptionTerm(component.container, transactionFeeLabel).textContent).toBe('0.001')
579+
580+
expect(getByDescriptionTerm(component.container, transactionSenderLabel).textContent).toBe(
581+
'QUESTA6XV2JZ2XAV3EK3GKBHYCJO57JWUX6L6ENHGNLR6UE3OPCUCT2WLI'
582+
)
583+
expect(getByDescriptionTerm(component.container, transactionReceiverLabel).textContent).toBe(
584+
'JQ76KXBOL3Z2EKRW43OPHOHKBZJQUULDAH33IIWDX2UWEYEMTKSX2PRS54'
585+
)
586+
expect(getByDescriptionTerm(component.container, assetLabel).textContent).toBe('1753701469 (DELETED)')
587+
expect(getByDescriptionTerm(component.container, transactionAmountLabel).textContent).toBe('1 DELETED')
588+
589+
const viewTransactionTabList = component.getByRole('tablist', { name: transactionDetailsLabel })
590+
expect(viewTransactionTabList).toBeTruthy()
591+
expect(
592+
component.getByRole('tabpanel', { name: visualTransactionDetailsTabLabel }).getAttribute('data-state'),
593+
'Visual tab should be active'
594+
).toBe('active')
595+
596+
// After click on the Table tab
597+
await user.click(getByRole(viewTransactionTabList, 'tab', { name: tableTransactionDetailsTabLabel }))
598+
const tableViewTab = component.getByRole('tabpanel', { name: tableTransactionDetailsTabLabel })
599+
await waitFor(() => expect(tableViewTab.getAttribute('data-state'), 'Table tab should be active').toBe('active'))
600+
601+
// Test the table data
602+
const dataRow = getAllByRole(tableViewTab, 'row')[1]
603+
expect(getAllByRole(dataRow, 'cell')[0].textContent).toBe('UFYPQDL...')
604+
expect(getAllByRole(dataRow, 'cell')[1].textContent).toBe('QUES...2WLI')
605+
expect(getAllByRole(dataRow, 'cell')[2].textContent).toBe('JQ76...RS54')
606+
expect(getAllByRole(dataRow, 'cell')[3].textContent).toBe('Asset Transfer')
607+
expect(getAllByRole(dataRow, 'cell')[4].textContent).toBe('1 DELETED')
608+
}
609+
)
610+
})
611+
})
552612
})

src/features/transactions/pages/transaction-page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { Transaction } from '../components/transaction'
55
import { useLoadableTransactionModelAtom } from '../data'
66
import { RenderLoadable } from '@/features/common/components/render-loadable'
77
import { cn } from '@/features/common/utils'
8+
import { is404 } from '@/utils/error'
89

910
const isValidTransactionId = (transactionId: string) => transactionId.length === 52
1011

1112
const transformError = (e: Error) => {
12-
if ('status' in e && e.status === 404) {
13+
if (is404(e)) {
1314
return new Error(transactionNotFoundMessage)
1415
}
1516

src/tests/object-mother/transaction-result.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AssetResult, TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
1+
import { AssetResult } from '@algorandfoundation/algokit-utils/types/indexer'
22
import { TransactionResultBuilder, transactionResultBuilder } from '../builders/transaction-result-builder'
33
import { TransactionType } from 'algosdk'
44

@@ -66,7 +66,7 @@ export const transactionResultMother = {
6666
sig: '4cwwpWiOnldnkW+M8Epwg2iaJvxdIvnUa9jM+uZxRcBTESRCD/BcsvbPVYrqEf6YwGCtsupNbNo6SwdUQRa2CQ==',
6767
},
6868
'tx-type': TransactionType.pay,
69-
} satisfies TransactionResult)
69+
})
7070
},
7171
['mainnet-ILDCD5Z64CYSLEZIHBG5DVME2ITJI2DIVZAPDPEWPCYMTRA5SVGA']: () => {
7272
// Payment transaction:
@@ -101,7 +101,7 @@ export const transactionResultMother = {
101101
},
102102
},
103103
'tx-type': TransactionType.pay,
104-
} satisfies TransactionResult)
104+
})
105105
},
106106
['mainnet-JBDSQEI37W5KWPQICT2IGCG2FWMUGJEUYYK3KFKNSYRNAXU2ARUA']: () => {
107107
// Asset transfer transaction
@@ -132,7 +132,7 @@ export const transactionResultMother = {
132132
sig: 'hk4FtHwulzfGDFq13MFsJfVS4UVdQAGhqFvsp9CjF9F6dD3V/P0XtW4V3cv2l8u0M1TDQoUsNbueW+SaQbD7DA==',
133133
},
134134
'tx-type': TransactionType.axfer,
135-
} satisfies TransactionResult)
135+
})
136136
},
137137
['mainnet-V7GQPE5TDMB4BIW2GCTPCBMXYMCF3HQGLYOYHGWP256GQHN5QAXQ']: () => {
138138
// Asset transfer transaction with close remainder. It is an asset opt-out transaction.
@@ -161,7 +161,7 @@ export const transactionResultMother = {
161161
sig: 'fK9vks0Sk2Sfa0PN+9wHSYYh2OKCFxSGBN2B4agVmVNoui17XcwXj4DbLJZWoknbVH/0gaKweKEYMIz4Oe8tDw==',
162162
},
163163
'tx-type': TransactionType.axfer,
164-
} satisfies TransactionResult)
164+
})
165165
},
166166
['mainnet-563MNGEL2OF4IBA7CFLIJNMBETT5QNKZURSLIONJBTJFALGYOAUA']: () => {
167167
// Asset opt-in
@@ -189,9 +189,38 @@ export const transactionResultMother = {
189189
'sender-rewards': 0,
190190
signature: { sig: 'eXs6In2s6DdoRIBHLesRRS9BX+UWykWX4YGPuTdOLJTn33NXM5paD7kZiB+4FQ27a+F7W2QEWJYU8QEzDHTVAQ==' },
191191
'tx-type': TransactionType.axfer,
192-
} satisfies TransactionResult)
192+
})
193+
},
194+
['mainnet-UFYPQDLWCVK3L5XVVHE7WBQWTW4YMHHKZSDIWXXV2AGCS646HTQA']: () => {
195+
return new TransactionResultBuilder({
196+
'asset-transfer-transaction': {
197+
amount: 1,
198+
'asset-id': 1753701469,
199+
'close-amount': 0,
200+
receiver: 'JQ76KXBOL3Z2EKRW43OPHOHKBZJQUULDAH33IIWDX2UWEYEMTKSX2PRS54',
201+
},
202+
'auth-addr': '7R7I3FN4ACXN2CEU5WPDZJZR475OYQP43QESNXA3NDC4SUQZZSI5OGCVME',
203+
'close-rewards': 0,
204+
'closing-amount': 0,
205+
'confirmed-round': 38008738,
206+
fee: 1000,
207+
'first-valid': 38008732,
208+
'genesis-hash': 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=',
209+
'genesis-id': 'mainnet-v1.0',
210+
group: 'XeNQmhxvtoWpue/7SAk6RNfuu/8Fp8tw8Nfn+HnIz00=',
211+
id: 'UFYPQDLWCVK3L5XVVHE7WBQWTW4YMHHKZSDIWXXV2AGCS646HTQA',
212+
'intra-round-offset': 16,
213+
'last-valid': 38009732,
214+
'receiver-rewards': 0,
215+
'round-time': 1713332366,
216+
sender: 'QUESTA6XV2JZ2XAV3EK3GKBHYCJO57JWUX6L6ENHGNLR6UE3OPCUCT2WLI',
217+
'sender-rewards': 0,
218+
signature: {
219+
sig: '2+WUUuw7ZHKjBFx3/ct+rdGQK/sv0u5ZSCN3+cnEE2KtvSbAMN3HYJaId9+tCWTlThGwHU0po4aCn1zFSh8hBg==',
220+
},
221+
'tx-type': TransactionType.axfer,
222+
})
193223
},
194-
195224
['testnet-VIXTUMAPT7NR4RB2WVOGMETW4QY43KIDA3HWDWWXS3UEDKGTEECQ']: () => {
196225
// Asset clawback
197226
return new TransactionResultBuilder({
@@ -219,6 +248,6 @@ export const transactionResultMother = {
219248
'sender-rewards': 0,
220249
signature: { sig: 'LYTng1fmA+JQ8AocqDfp/OBvrds/WXa936muT3b4Ym98qIzouEnbMf7cOj099GV+ABecBzmw6+JrzOH/WU7TDQ==' },
221250
'tx-type': TransactionType.axfer,
222-
} satisfies TransactionResult)
251+
})
223252
},
224253
}

src/tests/setup/mocks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ vi.mock('@/features/common/data', async () => {
2828
lookupBlock: vi.fn().mockReturnValue({
2929
do: vi.fn(),
3030
}),
31+
lookupAssetByID: vi.fn().mockReturnValue({
32+
do: vi.fn(),
33+
}),
3134
},
3235
}
3336
})

src/utils/error.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const asError = (error: unknown) => {
22
return error instanceof Error ? error : new Error(String(error))
33
}
4+
5+
export const is404 = (error: Error) => 'status' in error && error.status === 404

0 commit comments

Comments
 (0)