diff --git a/src/features/applications/components/application-details.tsx b/src/features/applications/components/application-details.tsx index a938d4ec3..27bcccfd2 100644 --- a/src/features/applications/components/application-details.tsx +++ b/src/features/applications/components/application-details.tsx @@ -23,6 +23,8 @@ import { applicationLocalStateByteLabel, applicationLocalStateUintLabel, applicationTransactionsLabel, + applicationJsonLabel, + applicationNameLabel, } from './labels' import { isDefined } from '@/utils/is-defined' import { ApplicationProgram } from './application-program' @@ -43,6 +45,12 @@ export function ApplicationDetails({ application }: Props) { dt: applicationIdLabel, dd: application.id, }, + application.name + ? { + dt: applicationNameLabel, + dd: application.name, + } + : undefined, { dt: applicationCreatorAccountLabel, dd: application.creator, @@ -76,7 +84,14 @@ export function ApplicationDetails({ application }: Props) { } : undefined, ], - [application.id, application.creator, application.account, application.globalStateSchema, application.localStateSchema] + [ + application.id, + application.name, + application.creator, + application.account, + application.globalStateSchema, + application.localStateSchema, + ] ).filter(isDefined) return ( @@ -141,6 +156,14 @@ export function ApplicationDetails({ application }: Props) { + + +

{applicationJsonLabel}

+
+
{application.json}
+
+
+
) } diff --git a/src/features/applications/components/labels.ts b/src/features/applications/components/labels.ts index 51f38a284..b6f20f5ca 100644 --- a/src/features/applications/components/labels.ts +++ b/src/features/applications/components/labels.ts @@ -1,5 +1,6 @@ export const applicationDetailsLabel = 'Application Details' export const applicationIdLabel = 'Application ID' +export const applicationNameLabel = 'Application Name' export const applicationCreatorAccountLabel = 'Creator' export const applicationAccountLabel = 'Account' export const applicationGlobalStateByteLabel = 'Global State Byte' @@ -25,3 +26,5 @@ export const applicationLiveTransactionsTabId = 'live-transactions' export const applicationLiveTransactionsTabLabel = 'Live Transactions' export const applicationHistoricalTransactionsTabId = 'historical-transactions' export const applicationHistoricalTransactionsTabLabel = 'Historical Transactions' + +export const applicationJsonLabel = 'Application JSON' diff --git a/src/features/applications/data/application-metadata.ts b/src/features/applications/data/application-metadata.ts new file mode 100644 index 000000000..e896e0013 --- /dev/null +++ b/src/features/applications/data/application-metadata.ts @@ -0,0 +1,45 @@ +import { ApplicationResult } from '@/features/accounts/data/types' +import { atomsInAtom } from '@/features/common/data/atoms-in-atom' +import { atom } from 'jotai' +import { ApplicationMetadataResult } from './types' +import { indexer } from '@/features/common/data' +import { flattenTransactionResult } from '@/features/transactions/utils/flatten-transaction-result' +import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { TransactionType } from 'algosdk' +import { base64ToUtf8 } from '@/utils/base64-to-utf8' +import { parseArc2 } from '@/features/transactions/mappers/arc-2' +import { parseJson } from '@/utils/parse-json' + +const createApplicationMetadataResultAtom = (applicationResult: ApplicationResult) => { + return atom | ApplicationMetadataResult>(async (_get) => { + // We only need to fetch the first page to find the application creation transaction + const transactionResults = await indexer + .searchForTransactions() + .applicationID(applicationResult.id) + .limit(3) + .do() + .then((res) => res.transactions as TransactionResult[]) + + const creationTransaction = transactionResults + .flatMap((txn) => flattenTransactionResult(txn)) + .find((txn) => txn['tx-type'] === TransactionType.appl && txn['created-application-index'] === applicationResult.id) + if (!creationTransaction) return null + + const text = base64ToUtf8(creationTransaction.note ?? '') + + const maybeArc2 = parseArc2(text) + if (maybeArc2 && maybeArc2.format === 'j') { + const arc2Data = parseJson(maybeArc2.data) + if (arc2Data && 'name' in arc2Data) { + return { name: arc2Data.name } + } + } + + return null + }) +} + +export const [applicationMetadataResultsAtom, getApplicationMetadataResultAtom] = atomsInAtom( + createApplicationMetadataResultAtom, + (applicationResult) => applicationResult.id +) diff --git a/src/features/applications/data/application-summary.ts b/src/features/applications/data/application-summary.ts new file mode 100644 index 000000000..8ad1d997f --- /dev/null +++ b/src/features/applications/data/application-summary.ts @@ -0,0 +1,12 @@ +import { JotaiStore } from '@/features/common/data/types' +import { ApplicationId } from './types' +import { atom } from 'jotai' +import { getApplicationResultAtom } from './application-result' +import { asApplicationSummary } from '../mappers' + +export const createApplicationSummaryAtom = (store: JotaiStore, applicationId: ApplicationId) => { + return atom(async (get) => { + const applicationResult = await get(getApplicationResultAtom(store, applicationId)) + return asApplicationSummary(applicationResult) + }) +} diff --git a/src/features/applications/data/application.ts b/src/features/applications/data/application.ts index c711d7e13..c8fc9bc1a 100644 --- a/src/features/applications/data/application.ts +++ b/src/features/applications/data/application.ts @@ -5,11 +5,13 @@ import { useMemo } from 'react' import { loadable } from 'jotai/utils' import { ApplicationId } from './types' import { getApplicationResultAtom } from './application-result' +import { getApplicationMetadataResultAtom } from './application-metadata' export const createApplicationAtom = (store: JotaiStore, applicationId: ApplicationId) => { return atom(async (get) => { const applicationResult = await get(getApplicationResultAtom(store, applicationId)) - return asApplication(applicationResult) + const applicationMetadata = await get(getApplicationMetadataResultAtom(store, applicationResult)) + return asApplication(applicationResult, applicationMetadata) }) } diff --git a/src/features/applications/data/types.ts b/src/features/applications/data/types.ts index 4f325a3ca..36488bcdc 100644 --- a/src/features/applications/data/types.ts +++ b/src/features/applications/data/types.ts @@ -1 +1,5 @@ export type ApplicationId = number + +export type ApplicationMetadataResult = { + name: string +} diff --git a/src/features/applications/mappers/index.ts b/src/features/applications/mappers/index.ts index 86f05c7e6..0a28bcc81 100644 --- a/src/features/applications/mappers/index.ts +++ b/src/features/applications/mappers/index.ts @@ -1,12 +1,21 @@ -import { Application, ApplicationGlobalStateType, ApplicationGlobalStateValue } from '../models' +import { Application, ApplicationGlobalStateType, ApplicationGlobalStateValue, ApplicationSummary } from '../models' import { ApplicationResult } from '@algorandfoundation/algokit-utils/types/indexer' import { getApplicationAddress, modelsv2, encodeAddress } from 'algosdk' import isUtf8 from 'isutf8' import { Buffer } from 'buffer' +import { ApplicationMetadataResult } from '../data/types' +import { asJson } from '@/utils/as-json' -export const asApplication = (application: ApplicationResult): Application => { +export const asApplicationSummary = (application: ApplicationResult): ApplicationSummary => { return { id: application.id, + } +} + +export const asApplication = (application: ApplicationResult, metadata?: ApplicationMetadataResult): Application => { + return { + id: application.id, + name: metadata?.name, creator: application.params.creator, account: getApplicationAddress(application.id), globalStateSchema: application.params['global-state-schema'] @@ -24,6 +33,7 @@ export const asApplication = (application: ApplicationResult): Application => { approvalProgram: application.params['approval-program'], clearStateProgram: application.params['clear-state-program'], globalState: asGlobalStateValue(application), + json: asJson(application), } } diff --git a/src/features/applications/models/index.ts b/src/features/applications/models/index.ts index 906252ffd..0777772db 100644 --- a/src/features/applications/models/index.ts +++ b/src/features/applications/models/index.ts @@ -1,7 +1,12 @@ import { ApplicationId } from '../data/types' +export type ApplicationSummary = { + id: ApplicationId +} + export type Application = { id: ApplicationId + name?: string account: string creator: string globalStateSchema?: ApplicationStateSchema @@ -9,7 +14,7 @@ export type Application = { approvalProgram: string clearStateProgram: string globalState: Map - // TODO: PD - ARC2 app stuff + json: string } export type ApplicationStateSchema = { diff --git a/src/features/applications/pages/application-page.test.tsx b/src/features/applications/pages/application-page.test.tsx index e868613f0..2a5501dec 100644 --- a/src/features/applications/pages/application-page.test.tsx +++ b/src/features/applications/pages/application-page.test.tsx @@ -24,10 +24,12 @@ import { applicationIdLabel, applicationLocalStateByteLabel, applicationLocalStateUintLabel, + applicationNameLabel, } from '../components/labels' import { descriptionListAssertion } from '@/tests/assertions/description-list-assertion' import { tableAssertion } from '@/tests/assertions/table-assertion' import { modelsv2, indexerModels } from 'algosdk' +import { transactionResultMother } from '@/tests/object-mother/transaction-result' describe('application-page', () => { describe('when rendering an application using an invalid application Id', () => { @@ -72,7 +74,7 @@ describe('application-page', () => { }) describe('when rendering an application', () => { - const applicationResult = applicationResultMother['mainner-80441968']().build() + const applicationResult = applicationResultMother['mainnet-80441968']().build() it('should be rendered with the correct data', () => { const myStore = createStore() @@ -119,6 +121,9 @@ describe('application-page', () => { }) ) ) + vi.mocked(indexer.searchForTransactions().applicationID(applicationResult.id).limit(3).do).mockImplementation(() => + Promise.resolve({ currentRound: 123, transactions: [], nextToken: '' }) + ) return executeComponentTest( () => { @@ -179,4 +184,37 @@ describe('application-page', () => { ) }) }) + + describe('when rendering an application that has app name following algokit standard', () => { + const applicationResult = applicationResultMother['mainnet-1196727051']().build() + const transactionResult = transactionResultMother['mainnet-XCXQW7J5G5QSPVU5JFYEELVIAAABPLZH2I36BMNVZLVHOA75MPAQ']().build() + + it('should be rendered with the correct app name', () => { + const myStore = createStore() + myStore.set(applicationResultsAtom, new Map([[applicationResult.id, atom(applicationResult)]])) + + vi.mocked(useParams).mockImplementation(() => ({ applicationId: applicationResult.id.toString() })) + vi.mocked(indexer.searchForTransactions().applicationID(applicationResult.id).limit(3).do).mockImplementation(() => + Promise.resolve({ currentRound: 123, transactions: [transactionResult], nextToken: '' }) + ) + + return executeComponentTest( + () => { + return render(, undefined, myStore) + }, + async (component) => { + await waitFor(async () => { + const detailsCard = component.getByLabelText(applicationDetailsLabel) + descriptionListAssertion({ + container: detailsCard, + items: [ + { term: applicationIdLabel, description: '1196727051' }, + { term: applicationNameLabel, description: 'cryptoless-JIUK4YAO2GU7UX36JHH35KWI4AJ3PDEYSRQ75PCJJKR5UBX6RQ6Y5UZSJQ' }, + ], + }) + }) + } + ) + }) + }) }) diff --git a/src/features/search/data/search.ts b/src/features/search/data/search.ts index fb12c4bab..6402d16c3 100644 --- a/src/features/search/data/search.ts +++ b/src/features/search/data/search.ts @@ -13,8 +13,8 @@ import { atomWithDebounce } from '@/features/common/data' import { isAddress } from '@/utils/is-address' import { isTransactionId } from '@/utils/is-transaction-id' import { isInteger } from '@/utils/is-integer' -import { createApplicationAtom } from '@/features/applications/data' import { syncedRoundAtom } from '@/features/blocks/data' +import { createApplicationSummaryAtom } from '@/features/applications/data/application-summary' const handle404 = (e: Error) => { if (is404(e)) { @@ -66,7 +66,7 @@ const createSearchAtoms = (store: JotaiStore) => { } const assetAtom = createAssetSummaryAtom(store, id) - const applicationAtom = createApplicationAtom(store, id) + const applicationAtom = createApplicationSummaryAtom(store, id) try { const [asset, application] = await Promise.all([ diff --git a/src/features/transactions/components/transaction-note.tsx b/src/features/transactions/components/transaction-note.tsx index 199056d2b..9a1683de6 100644 --- a/src/features/transactions/components/transaction-note.tsx +++ b/src/features/transactions/components/transaction-note.tsx @@ -1,38 +1,15 @@ import { cn } from '@/features/common/utils' -import { Arc2TransactionNote } from '@algorandfoundation/algokit-utils/types/transaction' import { useMemo } from 'react' import { DescriptionList } from '@/features/common/components/description-list' import { base64ToUtf8 } from '@/utils/base64-to-utf8' import { OverflowAutoTabsContent, Tabs, TabsList, TabsTrigger } from '@/features/common/components/tabs' +import { parseArc2 } from '../mappers/arc-2' +import { parseJson } from '@/utils/parse-json' type TransactionNoteProps = { note: string } -function parseJson(maybeJson: string) { - try { - const json = JSON.parse(maybeJson) - if (json && typeof json === 'object') { - return json - } - } catch (e) { - // ignore - } -} - -// Based on the ARC-2 spec https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md#specification -const arc2Regex = /^([a-zA-Z0-9][a-zA-Z0-9_/@.-]{4,31}):([mjbu]{1})(.*)$/ -function parseArc2(maybeArc2: string) { - const result = maybeArc2.match(arc2Regex) - if (result && result.length === 4) { - return { - dAppName: result[1], - format: result[2] as 'm' | 'b' | 'u' | 'j', - data: result[3], - } satisfies Arc2TransactionNote - } -} - const base64NoteTabId = 'base64' const textNoteTabId = 'text' const jsonNoteTabId = 'json' diff --git a/src/features/transactions/mappers/arc-2.ts b/src/features/transactions/mappers/arc-2.ts new file mode 100644 index 000000000..595505433 --- /dev/null +++ b/src/features/transactions/mappers/arc-2.ts @@ -0,0 +1,15 @@ +import { Arc2TransactionNote } from '@algorandfoundation/algokit-utils/types/transaction' + +// Based on the ARC-2 spec https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md#specification +const arc2Regex = /^([a-zA-Z0-9][a-zA-Z0-9_/@.-]{4,31}):([mjbu]{1})(.*)$/ + +export function parseArc2(maybeArc2: string) { + const result = maybeArc2.match(arc2Regex) + if (result && result.length === 4) { + return { + dAppName: result[1], + format: result[2] as 'm' | 'b' | 'u' | 'j', + data: result[3], + } satisfies Arc2TransactionNote + } +} diff --git a/src/tests/object-mother/application-result.ts b/src/tests/object-mother/application-result.ts index aa29ce629..4ab0eafbd 100644 --- a/src/tests/object-mother/application-result.ts +++ b/src/tests/object-mother/application-result.ts @@ -5,7 +5,7 @@ export const applicationResultMother = { basic: () => { return applicationResultBuilder() }, - 'mainner-80441968': () => { + 'mainnet-80441968': () => { return new ApplicationResultBuilder({ id: 80441968, params: { @@ -35,6 +35,53 @@ export const applicationResultMother = { }, }) }, + 'mainnet-1196727051': () => { + return new ApplicationResultBuilder({ + 'created-at-round': 32218016, + deleted: false, + id: 1196727051, + params: { + 'approval-program': + 'CSAEAAEGAiYJDG5mdGlja2V0X2FwcAABAA9tYW5hZ2VyX2FkZHJlc3MBAQQuU0kZBGg0o6oSbWV0aG9kX3Blcm1pc3Npb25zB2FpcmxpbmUxGyISQAEtNhoAgAThNZDwEkABETYaAIAEnNbuoRJAAPU2GgCABF5iYz4SQADZNhoAgAS2olF1EkAAvTYaAIAE+Qj2KRJAAKE2GgCABB0UDY4SQACFNhoAgATG43mkEkAAaTYaACcFEkAAUTYaAIAEnvUJRhJAADU2GgCABE+i3akSQAAZNhoAJwYSQAABADEZIhIxGCITEESIBBYjQzEZIhIxGCITEESIA+wjQzEZIhIxGCITEESIA7IjQzEZIhIxGCITEESIA34jQzEZIhIxGCITEESIA0wjQzEZIhIxGCITEESIAxAjQzEZIhIxGCISEESIAuAjQzEZIhIxGCITEESIArcjQzEZIhIxGCITEESIApcjQzEZIhIxGCITEESIAncjQzEZIhIxGCITEESIAlcjQzEZIxJAADYxGSUSQAAlMRmBBBJAABMxGYEFEkAAAQAxGCITRIgAViNDMRgiE0SIAEEjQzEYIhNEiABSI0MxGCITRIgAQyNDigIBi/4yCGFAAAQiQgAbi/4iJwdjNQE1ADQBQAAHIov/U0IABTQAQv/1iYoAADEAMgkSRCNDigAAMQAyCRJEI0OKAAAjQ4oAACNDigEAMQAyCRJEK4v/wBxniYoBADEAK2QSRLEkshAjshmL/8AyshgisgGziYoBADEAK2QSRLEkshAlshmL/8AyshgisgGziYoCADEAK2QSRIv+wBwnB4v/ZomKAgGL/icIZBJAAAqL/ov/iP9JQgABI4mKAwAoi/7AMmcri/1nJwiL/2cjQ4oFADEAIoj/y0SxJLIQKGSyGIAEWOGHaLIai/uyGov8shqL/bIai/6yGov/shoisgGziYoDADEAJIj/mESxJLIQKGSyGIv/wByyHIv+wDCyMIAErnlpMLIai/2yGiqyGicEshoisgGziYoDADEAgQOI/2FEsSSyEChkshiL/sAcshyL/8AcshyL/cAwsjAnBbIaKrIaJwSyGoABArIaIrIBs4mKBAAxAIEFiP8nRLEkshAoZLIYi/zAMLIwgAQ7tliRshoqshqL/bIai/4WVwQAshqL/7IaIrIBs4mKAgAxAIEEiP7uRLEkshAoZLIYi/7AMLIwgARMfzwNshoqshoqIov/VrIaIrIBs4mKAwAxACWI/r9EsSSyEChkshiL/8AcshyL/sAwsjAnBrIai/2yGiqyGicEshoisgGziYoAACI2GgEiVYwAiwCI/i2JigAAIjYaASJVjACLAIj+LImKAAAiNhoBIlWMAIsAiP42iYoAACJJNhoBIlWMADYaAheMAYsAiwGI/jeJigAAKSIpNhoBjAA2GgIiVYwBNhoDjAKLAIsBiwKI/kGJigAAKUcENhoBjAA2GgKMATYaA4wCNhoEjAM2GgWMBIsAiwGLAosDiwSI/iiJigAAKSJJNhoBjAA2GgIiVYwBNhoDIlWMAosAiwGLAoj+OImKAAAiRwI2GgEiVYwANhoCIlWMATYaAyJVjAKLAIsBiwKI/kmJigAAIikiKTYaASJVjAA2GgKMATYaAyJajAI2GgSMA4sAiwGLAosDiP5YiYoAACJJNhoBIlWMADYaAiJVjAGLAIsBiP52iYoAACkiSTYaAYwANhoCIlWMATYaAyJVjAKLAIsBiwKI/oOJ', + 'clear-state-program': 'CYEAQw==', + creator: '52MVNW6FNW7L6W7IAKSROD5FYZGZNLVKT6WUWNUFEE3DT737RYIIL2YQ3Y', + 'global-state': [ + toTealKeyValue({ + key: 'bWFuYWdlcl9hZGRyZXNz', + value: { + bytes: '0QGHvZ1GkfgBxdPm8vbFxBpukSHn/8UGJmPuEFl9eDk=', + type: 1, + uint: 0, + }, + }), + toTealKeyValue({ + key: 'YWlybGluZQ==', + value: { + bytes: 'SiiuYA7RqfpffknPvqrI4BO3jJiUYf68SUqj2gb+jD0=', + type: 1, + uint: 0, + }, + }), + toTealKeyValue({ + key: 'bmZ0aWNrZXRfYXBw', + value: { + bytes: '', + type: 2, + uint: 1196710954, + }, + }), + ], + 'global-state-schema': { + 'num-byte-slice': 2, + 'num-uint': 1, + }, + 'local-state-schema': { + 'num-byte-slice': 0, + 'num-uint': 1, + }, + }, + }) + }, } const toTealKeyValue = ({ key, value }: { key: string; value: { type: number; uint: number; bytes: string } }) => diff --git a/src/tests/object-mother/transaction-result.ts b/src/tests/object-mother/transaction-result.ts index fb593d275..37a98a172 100644 --- a/src/tests/object-mother/transaction-result.ts +++ b/src/tests/object-mother/transaction-result.ts @@ -1281,4 +1281,77 @@ export const transactionResultMother = { 'tx-type': TransactionType.acfg, }) }, + ['mainnet-XCXQW7J5G5QSPVU5JFYEELVIAAABPLZH2I36BMNVZLVHOA75MPAQ']: () => { + return new TransactionResultBuilder({ + 'application-transaction': { + accounts: [], + 'application-args': [ + '+Qj2KQ==', + '0QGHvZ1GkfgBxdPm8vbFxBpukSHn/8UGJmPuEFl9eDk=', + 'AQ==', + 'SiiuYA7RqfpffknPvqrI4BO3jJiUYf68SUqj2gb+jD0=', + ], + 'application-id': 0, + 'approval-program': + 'CSAEAAEGAiYJDG5mdGlja2V0X2FwcAABAA9tYW5hZ2VyX2FkZHJlc3MBAQQuU0kZBGg0o6oSbWV0aG9kX3Blcm1pc3Npb25zB2FpcmxpbmUxGyISQAEtNhoAgAThNZDwEkABETYaAIAEnNbuoRJAAPU2GgCABF5iYz4SQADZNhoAgAS2olF1EkAAvTYaAIAE+Qj2KRJAAKE2GgCABB0UDY4SQACFNhoAgATG43mkEkAAaTYaACcFEkAAUTYaAIAEnvUJRhJAADU2GgCABE+i3akSQAAZNhoAJwYSQAABADEZIhIxGCITEESIBBYjQzEZIhIxGCITEESIA+wjQzEZIhIxGCITEESIA7IjQzEZIhIxGCITEESIA34jQzEZIhIxGCITEESIA0wjQzEZIhIxGCITEESIAxAjQzEZIhIxGCISEESIAuAjQzEZIhIxGCITEESIArcjQzEZIhIxGCITEESIApcjQzEZIhIxGCITEESIAncjQzEZIhIxGCITEESIAlcjQzEZIxJAADYxGSUSQAAlMRmBBBJAABMxGYEFEkAAAQAxGCITRIgAViNDMRgiE0SIAEEjQzEYIhNEiABSI0MxGCITRIgAQyNDigIBi/4yCGFAAAQiQgAbi/4iJwdjNQE1ADQBQAAHIov/U0IABTQAQv/1iYoAADEAMgkSRCNDigAAMQAyCRJEI0OKAAAjQ4oAACNDigEAMQAyCRJEK4v/wBxniYoBADEAK2QSRLEkshAjshmL/8AyshgisgGziYoBADEAK2QSRLEkshAlshmL/8AyshgisgGziYoCADEAK2QSRIv+wBwnB4v/ZomKAgGL/icIZBJAAAqL/ov/iP9JQgABI4mKAwAoi/7AMmcri/1nJwiL/2cjQ4oFADEAIoj/y0SxJLIQKGSyGIAEWOGHaLIai/uyGov8shqL/bIai/6yGov/shoisgGziYoDADEAJIj/mESxJLIQKGSyGIv/wByyHIv+wDCyMIAErnlpMLIai/2yGiqyGicEshoisgGziYoDADEAgQOI/2FEsSSyEChkshiL/sAcshyL/8AcshyL/cAwsjAnBbIaKrIaJwSyGoABArIaIrIBs4mKBAAxAIEFiP8nRLEkshAoZLIYi/zAMLIwgAQ7tliRshoqshqL/bIai/4WVwQAshqL/7IaIrIBs4mKAgAxAIEEiP7uRLEkshAoZLIYi/7AMLIwgARMfzwNshoqshoqIov/VrIaIrIBs4mKAwAxACWI/r9EsSSyEChkshiL/8AcshyL/sAwsjAnBrIai/2yGiqyGicEshoisgGziYoAACI2GgEiVYwAiwCI/i2JigAAIjYaASJVjACLAIj+LImKAAAiNhoBIlWMAIsAiP42iYoAACJJNhoBIlWMADYaAheMAYsAiwGI/jeJigAAKSIpNhoBjAA2GgIiVYwBNhoDjAKLAIsBiwKI/kGJigAAKUcENhoBjAA2GgKMATYaA4wCNhoEjAM2GgWMBIsAiwGLAosDiwSI/iiJigAAKSJJNhoBjAA2GgIiVYwBNhoDIlWMAosAiwGLAoj+OImKAAAiRwI2GgEiVYwANhoCIlWMATYaAyJVjAKLAIsBiwKI/kmJigAAIikiKTYaASJVjAA2GgKMATYaAyJajAI2GgSMA4sAiwGLAosDiP5YiYoAACJJNhoBIlWMADYaAiJVjAGLAIsBiP52iYoAACkiSTYaAYwANhoCIlWMATYaAyJVjAKLAIsBiwKI/oOJ', + 'clear-state-program': 'CYEAQw==', + 'foreign-apps': [1196710954], + 'foreign-assets': [], + 'global-state-schema': { + 'num-byte-slice': 2, + 'num-uint': 1, + }, + 'local-state-schema': { + 'num-byte-slice': 0, + 'num-uint': 1, + }, + 'on-completion': 'noop', + }, + 'close-rewards': 0, + 'closing-amount': 0, + 'confirmed-round': 32218016, + 'created-application-index': 1196727051, + fee: 1000, + 'first-valid': 32218000, + 'genesis-hash': 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + 'genesis-id': 'mainnet-v1.0', + 'global-state-delta': [ + { + key: 'YWlybGluZQ==', + value: { + action: 1, + bytes: 'SiiuYA7RqfpffknPvqrI4BO3jJiUYf68SUqj2gb+jD0=', + uint: 0, + }, + }, + { + key: 'bWFuYWdlcl9hZGRyZXNz', + value: { + action: 1, + bytes: '0QGHvZ1GkfgBxdPm8vbFxBpukSHn/8UGJmPuEFl9eDk=', + uint: 0, + }, + }, + { + key: 'bmZ0aWNrZXRfYXBw', + value: { + action: 2, + uint: 1196710954, + }, + }, + ], + id: 'XCXQW7J5G5QSPVU5JFYEELVIAAABPLZH2I36BMNVZLVHOA75MPAQ', + 'intra-round-offset': 18, + 'last-valid': 32219000, + note: 'QUxHT0tJVF9ERVBMT1lFUjpqeyJuYW1lIjogImNyeXB0b2xlc3MtSklVSzRZQU8yR1U3VVgzNkpISDM1S1dJNEFKM1BERVlTUlE3NVBDSkpLUjVVQlg2UlE2WTVVWlNKUSIsICJ2ZXJzaW9uIjogInYxLjAiLCAiZGVsZXRhYmxlIjogbnVsbCwgInVwZGF0YWJsZSI6IG51bGx9', + 'receiver-rewards': 0, + 'round-time': 1695154915, + sender: '52MVNW6FNW7L6W7IAKSROD5FYZGZNLVKT6WUWNUFEE3DT737RYIIL2YQ3Y', + 'sender-rewards': 0, + signature: { + sig: 'hogSpsFw9RMBA7wlp1bf66qInAlIQ9Q762bWwd/Wah2o5jeZ0dNp29QhXsposgCalhThD5PLVrr6N77vdWZICg==', + }, + 'tx-type': 'appl', + } as unknown as TransactionResult) + }, } diff --git a/src/tests/setup/mocks.ts b/src/tests/setup/mocks.ts index e688c4036..619c3dba2 100644 --- a/src/tests/setup/mocks.ts +++ b/src/tests/setup/mocks.ts @@ -53,6 +53,11 @@ vi.mock('@/features/common/data', async () => { }), }), }), + applicationID: vi.fn().mockReturnValue({ + limit: vi.fn().mockReturnValue({ + do: vi.fn().mockReturnValue({ then: vi.fn() }), + }), + }), }), lookupApplications: vi.fn().mockReturnValue({ includeAll: vi.fn().mockReturnValue({ diff --git a/src/utils/as-json.ts b/src/utils/as-json.ts index 20e91d622..35103f04c 100644 --- a/src/utils/as-json.ts +++ b/src/utils/as-json.ts @@ -1,2 +1 @@ -export const asJson = (transactionResult: unknown) => - JSON.stringify(transactionResult, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2) +export const asJson = (indexerResult: unknown) => JSON.stringify(indexerResult, (_, v) => (typeof v === 'bigint' ? v.toString() : v), 2) diff --git a/src/utils/parse-json.ts b/src/utils/parse-json.ts new file mode 100644 index 000000000..d5d49b1eb --- /dev/null +++ b/src/utils/parse-json.ts @@ -0,0 +1,10 @@ +export function parseJson(maybeJson: string) { + try { + const json = JSON.parse(maybeJson) + if (json && typeof json === 'object') { + return json + } + } catch (e) { + // ignore + } +}