Skip to content

Commit

Permalink
feat: show application name if it follows algokit standard
Browse files Browse the repository at this point in the history
  • Loading branch information
PatrickDinh authored May 20, 2024
1 parent 3aafc0d commit 7dc66b8
Show file tree
Hide file tree
Showing 17 changed files with 304 additions and 36 deletions.
25 changes: 24 additions & 1 deletion src/features/applications/components/application-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
applicationLocalStateByteLabel,
applicationLocalStateUintLabel,
applicationTransactionsLabel,
applicationJsonLabel,
applicationNameLabel,
} from './labels'
import { isDefined } from '@/utils/is-defined'
import { ApplicationProgram } from './application-program'
Expand All @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -141,6 +156,14 @@ export function ApplicationDetails({ application }: Props) {
</Tabs>
</CardContent>
</Card>
<Card className={cn('p-4')}>
<CardContent className={cn('text-sm space-y-2')}>
<h1 className={cn('text-2xl text-primary font-bold')}>{applicationJsonLabel}</h1>
<div className={cn('border-solid border-2 border-border h-96 grid')}>
<pre className={cn('overflow-scroll p-4')}>{application.json}</pre>
</div>
</CardContent>
</Card>
</div>
)
}
3 changes: 3 additions & 0 deletions src/features/applications/components/labels.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
45 changes: 45 additions & 0 deletions src/features/applications/data/application-metadata.ts
Original file line number Diff line number Diff line change
@@ -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<Promise<ApplicationMetadataResult> | 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
)
12 changes: 12 additions & 0 deletions src/features/applications/data/application-summary.ts
Original file line number Diff line number Diff line change
@@ -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)
})
}
4 changes: 3 additions & 1 deletion src/features/applications/data/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}

Expand Down
4 changes: 4 additions & 0 deletions src/features/applications/data/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export type ApplicationId = number

export type ApplicationMetadataResult = {
name: string
}
14 changes: 12 additions & 2 deletions src/features/applications/mappers/index.ts
Original file line number Diff line number Diff line change
@@ -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']
Expand All @@ -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),
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/features/applications/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { ApplicationId } from '../data/types'

export type ApplicationSummary = {
id: ApplicationId
}

export type Application = {
id: ApplicationId
name?: string
account: string
creator: string
globalStateSchema?: ApplicationStateSchema
localStateSchema?: ApplicationStateSchema
approvalProgram: string
clearStateProgram: string
globalState: Map<string, ApplicationGlobalStateValue>
// TODO: PD - ARC2 app stuff
json: string
}

export type ApplicationStateSchema = {
Expand Down
40 changes: 39 additions & 1 deletion src/features/applications/pages/application-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(
() => {
Expand Down Expand Up @@ -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(<ApplicationPage />, 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' },
],
})
})
}
)
})
})
})
4 changes: 2 additions & 2 deletions src/features/search/data/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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([
Expand Down
27 changes: 2 additions & 25 deletions src/features/transactions/components/transaction-note.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
15 changes: 15 additions & 0 deletions src/features/transactions/mappers/arc-2.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 7dc66b8

Please sign in to comment.