Skip to content

Commit 7dc66b8

Browse files
authored
feat: show application name if it follows algokit standard
1 parent 3aafc0d commit 7dc66b8

File tree

17 files changed

+304
-36
lines changed

17 files changed

+304
-36
lines changed

src/features/applications/components/application-details.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
applicationLocalStateByteLabel,
2424
applicationLocalStateUintLabel,
2525
applicationTransactionsLabel,
26+
applicationJsonLabel,
27+
applicationNameLabel,
2628
} from './labels'
2729
import { isDefined } from '@/utils/is-defined'
2830
import { ApplicationProgram } from './application-program'
@@ -43,6 +45,12 @@ export function ApplicationDetails({ application }: Props) {
4345
dt: applicationIdLabel,
4446
dd: application.id,
4547
},
48+
application.name
49+
? {
50+
dt: applicationNameLabel,
51+
dd: application.name,
52+
}
53+
: undefined,
4654
{
4755
dt: applicationCreatorAccountLabel,
4856
dd: application.creator,
@@ -76,7 +84,14 @@ export function ApplicationDetails({ application }: Props) {
7684
}
7785
: undefined,
7886
],
79-
[application.id, application.creator, application.account, application.globalStateSchema, application.localStateSchema]
87+
[
88+
application.id,
89+
application.name,
90+
application.creator,
91+
application.account,
92+
application.globalStateSchema,
93+
application.localStateSchema,
94+
]
8095
).filter(isDefined)
8196

8297
return (
@@ -141,6 +156,14 @@ export function ApplicationDetails({ application }: Props) {
141156
</Tabs>
142157
</CardContent>
143158
</Card>
159+
<Card className={cn('p-4')}>
160+
<CardContent className={cn('text-sm space-y-2')}>
161+
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationJsonLabel}</h1>
162+
<div className={cn('border-solid border-2 border-border h-96 grid')}>
163+
<pre className={cn('overflow-scroll p-4')}>{application.json}</pre>
164+
</div>
165+
</CardContent>
166+
</Card>
144167
</div>
145168
)
146169
}

src/features/applications/components/labels.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const applicationDetailsLabel = 'Application Details'
22
export const applicationIdLabel = 'Application ID'
3+
export const applicationNameLabel = 'Application Name'
34
export const applicationCreatorAccountLabel = 'Creator'
45
export const applicationAccountLabel = 'Account'
56
export const applicationGlobalStateByteLabel = 'Global State Byte'
@@ -25,3 +26,5 @@ export const applicationLiveTransactionsTabId = 'live-transactions'
2526
export const applicationLiveTransactionsTabLabel = 'Live Transactions'
2627
export const applicationHistoricalTransactionsTabId = 'historical-transactions'
2728
export const applicationHistoricalTransactionsTabLabel = 'Historical Transactions'
29+
30+
export const applicationJsonLabel = 'Application JSON'
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ApplicationResult } from '@/features/accounts/data/types'
2+
import { atomsInAtom } from '@/features/common/data/atoms-in-atom'
3+
import { atom } from 'jotai'
4+
import { ApplicationMetadataResult } from './types'
5+
import { indexer } from '@/features/common/data'
6+
import { flattenTransactionResult } from '@/features/transactions/utils/flatten-transaction-result'
7+
import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer'
8+
import { TransactionType } from 'algosdk'
9+
import { base64ToUtf8 } from '@/utils/base64-to-utf8'
10+
import { parseArc2 } from '@/features/transactions/mappers/arc-2'
11+
import { parseJson } from '@/utils/parse-json'
12+
13+
const createApplicationMetadataResultAtom = (applicationResult: ApplicationResult) => {
14+
return atom<Promise<ApplicationMetadataResult> | ApplicationMetadataResult>(async (_get) => {
15+
// We only need to fetch the first page to find the application creation transaction
16+
const transactionResults = await indexer
17+
.searchForTransactions()
18+
.applicationID(applicationResult.id)
19+
.limit(3)
20+
.do()
21+
.then((res) => res.transactions as TransactionResult[])
22+
23+
const creationTransaction = transactionResults
24+
.flatMap((txn) => flattenTransactionResult(txn))
25+
.find((txn) => txn['tx-type'] === TransactionType.appl && txn['created-application-index'] === applicationResult.id)
26+
if (!creationTransaction) return null
27+
28+
const text = base64ToUtf8(creationTransaction.note ?? '')
29+
30+
const maybeArc2 = parseArc2(text)
31+
if (maybeArc2 && maybeArc2.format === 'j') {
32+
const arc2Data = parseJson(maybeArc2.data)
33+
if (arc2Data && 'name' in arc2Data) {
34+
return { name: arc2Data.name }
35+
}
36+
}
37+
38+
return null
39+
})
40+
}
41+
42+
export const [applicationMetadataResultsAtom, getApplicationMetadataResultAtom] = atomsInAtom(
43+
createApplicationMetadataResultAtom,
44+
(applicationResult) => applicationResult.id
45+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { JotaiStore } from '@/features/common/data/types'
2+
import { ApplicationId } from './types'
3+
import { atom } from 'jotai'
4+
import { getApplicationResultAtom } from './application-result'
5+
import { asApplicationSummary } from '../mappers'
6+
7+
export const createApplicationSummaryAtom = (store: JotaiStore, applicationId: ApplicationId) => {
8+
return atom(async (get) => {
9+
const applicationResult = await get(getApplicationResultAtom(store, applicationId))
10+
return asApplicationSummary(applicationResult)
11+
})
12+
}

src/features/applications/data/application.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { useMemo } from 'react'
55
import { loadable } from 'jotai/utils'
66
import { ApplicationId } from './types'
77
import { getApplicationResultAtom } from './application-result'
8+
import { getApplicationMetadataResultAtom } from './application-metadata'
89

910
export const createApplicationAtom = (store: JotaiStore, applicationId: ApplicationId) => {
1011
return atom(async (get) => {
1112
const applicationResult = await get(getApplicationResultAtom(store, applicationId))
12-
return asApplication(applicationResult)
13+
const applicationMetadata = await get(getApplicationMetadataResultAtom(store, applicationResult))
14+
return asApplication(applicationResult, applicationMetadata)
1315
})
1416
}
1517

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
export type ApplicationId = number
2+
3+
export type ApplicationMetadataResult = {
4+
name: string
5+
}

src/features/applications/mappers/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
import { Application, ApplicationGlobalStateType, ApplicationGlobalStateValue } from '../models'
1+
import { Application, ApplicationGlobalStateType, ApplicationGlobalStateValue, ApplicationSummary } from '../models'
22
import { ApplicationResult } from '@algorandfoundation/algokit-utils/types/indexer'
33
import { getApplicationAddress, modelsv2, encodeAddress } from 'algosdk'
44
import isUtf8 from 'isutf8'
55
import { Buffer } from 'buffer'
6+
import { ApplicationMetadataResult } from '../data/types'
7+
import { asJson } from '@/utils/as-json'
68

7-
export const asApplication = (application: ApplicationResult): Application => {
9+
export const asApplicationSummary = (application: ApplicationResult): ApplicationSummary => {
810
return {
911
id: application.id,
12+
}
13+
}
14+
15+
export const asApplication = (application: ApplicationResult, metadata?: ApplicationMetadataResult): Application => {
16+
return {
17+
id: application.id,
18+
name: metadata?.name,
1019
creator: application.params.creator,
1120
account: getApplicationAddress(application.id),
1221
globalStateSchema: application.params['global-state-schema']
@@ -24,6 +33,7 @@ export const asApplication = (application: ApplicationResult): Application => {
2433
approvalProgram: application.params['approval-program'],
2534
clearStateProgram: application.params['clear-state-program'],
2635
globalState: asGlobalStateValue(application),
36+
json: asJson(application),
2737
}
2838
}
2939

src/features/applications/models/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { ApplicationId } from '../data/types'
22

3+
export type ApplicationSummary = {
4+
id: ApplicationId
5+
}
6+
37
export type Application = {
48
id: ApplicationId
9+
name?: string
510
account: string
611
creator: string
712
globalStateSchema?: ApplicationStateSchema
813
localStateSchema?: ApplicationStateSchema
914
approvalProgram: string
1015
clearStateProgram: string
1116
globalState: Map<string, ApplicationGlobalStateValue>
12-
// TODO: PD - ARC2 app stuff
17+
json: string
1318
}
1419

1520
export type ApplicationStateSchema = {

src/features/applications/pages/application-page.test.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import {
2424
applicationIdLabel,
2525
applicationLocalStateByteLabel,
2626
applicationLocalStateUintLabel,
27+
applicationNameLabel,
2728
} from '../components/labels'
2829
import { descriptionListAssertion } from '@/tests/assertions/description-list-assertion'
2930
import { tableAssertion } from '@/tests/assertions/table-assertion'
3031
import { modelsv2, indexerModels } from 'algosdk'
32+
import { transactionResultMother } from '@/tests/object-mother/transaction-result'
3133

3234
describe('application-page', () => {
3335
describe('when rendering an application using an invalid application Id', () => {
@@ -72,7 +74,7 @@ describe('application-page', () => {
7274
})
7375

7476
describe('when rendering an application', () => {
75-
const applicationResult = applicationResultMother['mainner-80441968']().build()
77+
const applicationResult = applicationResultMother['mainnet-80441968']().build()
7678

7779
it('should be rendered with the correct data', () => {
7880
const myStore = createStore()
@@ -119,6 +121,9 @@ describe('application-page', () => {
119121
})
120122
)
121123
)
124+
vi.mocked(indexer.searchForTransactions().applicationID(applicationResult.id).limit(3).do).mockImplementation(() =>
125+
Promise.resolve({ currentRound: 123, transactions: [], nextToken: '' })
126+
)
122127

123128
return executeComponentTest(
124129
() => {
@@ -179,4 +184,37 @@ describe('application-page', () => {
179184
)
180185
})
181186
})
187+
188+
describe('when rendering an application that has app name following algokit standard', () => {
189+
const applicationResult = applicationResultMother['mainnet-1196727051']().build()
190+
const transactionResult = transactionResultMother['mainnet-XCXQW7J5G5QSPVU5JFYEELVIAAABPLZH2I36BMNVZLVHOA75MPAQ']().build()
191+
192+
it('should be rendered with the correct app name', () => {
193+
const myStore = createStore()
194+
myStore.set(applicationResultsAtom, new Map([[applicationResult.id, atom(applicationResult)]]))
195+
196+
vi.mocked(useParams).mockImplementation(() => ({ applicationId: applicationResult.id.toString() }))
197+
vi.mocked(indexer.searchForTransactions().applicationID(applicationResult.id).limit(3).do).mockImplementation(() =>
198+
Promise.resolve({ currentRound: 123, transactions: [transactionResult], nextToken: '' })
199+
)
200+
201+
return executeComponentTest(
202+
() => {
203+
return render(<ApplicationPage />, undefined, myStore)
204+
},
205+
async (component) => {
206+
await waitFor(async () => {
207+
const detailsCard = component.getByLabelText(applicationDetailsLabel)
208+
descriptionListAssertion({
209+
container: detailsCard,
210+
items: [
211+
{ term: applicationIdLabel, description: '1196727051' },
212+
{ term: applicationNameLabel, description: 'cryptoless-JIUK4YAO2GU7UX36JHH35KWI4AJ3PDEYSRQ75PCJJKR5UBX6RQ6Y5UZSJQ' },
213+
],
214+
})
215+
})
216+
}
217+
)
218+
})
219+
})
182220
})

src/features/search/data/search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { atomWithDebounce } from '@/features/common/data'
1313
import { isAddress } from '@/utils/is-address'
1414
import { isTransactionId } from '@/utils/is-transaction-id'
1515
import { isInteger } from '@/utils/is-integer'
16-
import { createApplicationAtom } from '@/features/applications/data'
1716
import { syncedRoundAtom } from '@/features/blocks/data'
17+
import { createApplicationSummaryAtom } from '@/features/applications/data/application-summary'
1818

1919
const handle404 = (e: Error) => {
2020
if (is404(e)) {
@@ -66,7 +66,7 @@ const createSearchAtoms = (store: JotaiStore) => {
6666
}
6767

6868
const assetAtom = createAssetSummaryAtom(store, id)
69-
const applicationAtom = createApplicationAtom(store, id)
69+
const applicationAtom = createApplicationSummaryAtom(store, id)
7070

7171
try {
7272
const [asset, application] = await Promise.all([

0 commit comments

Comments
 (0)