Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render struct in app call #257

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/features/abi-methods/components/abi-struct-value.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { AbiValue } from '@/features/abi-methods/components/abi-value'
import { AbiStructValue as AbiStructValueModel } from '@/features/abi-methods/models'

type Props = {
struct: AbiStructValueModel
}

export function AbiStructValue({ struct }: Props) {
if (struct.multiline) {
return (
<>
<span>{'{'}</span>
<ul className="pl-4">
{struct.values.map((item, index, array) => (
<li key={index}>
<span>{item.name}: </span>
<AbiValue abiValue={item.value} />
{index < array.length - 1 ? <span>{', '}</span> : null}
</li>
))}
</ul>
<span>{'}'}</span>
</>
)
} else {
return (
<>
<span>{'{'}</span>
<div className="inline">
{struct.values.map((item, index, array) => (
<div className="inline" key={index}>
<span>{item.name}: </span>
<AbiValue abiValue={item.value} />
{index < array.length - 1 ? <span>{', '}</span> : null}
</div>
))}
</div>
<span>{'}'}</span>
</>
)
}
}
4 changes: 4 additions & 0 deletions src/features/abi-methods/components/abi-value.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AbiArrayValue } from '@/features/abi-methods/components/abi-array-value
import { AbiTupleValue } from '@/features/abi-methods/components/abi-tuple-value'
import { AbiValue as AbiValueModel, AbiType } from '@/features/abi-methods/models'
import { AccountLink } from '@/features/accounts/components/account-link'
import { AbiStructValue } from '@/features/abi-methods/components/abi-struct-value'

type Props = {
abiValue: AbiValueModel
Expand All @@ -11,6 +12,9 @@ export function AbiValue({ abiValue }: Props) {
if (abiValue.type === AbiType.Tuple) {
return <AbiTupleValue tuple={abiValue} />
}
if (abiValue.type === AbiType.Struct) {
return <AbiStructValue struct={abiValue} />
}
if (abiValue.type === AbiType.Array) {
return <AbiArrayValue array={abiValue} />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { useMemo, useCallback } from 'react'

type Props = {
arguments: AbiMethodArgument[]
multiLine: boolean
multiline: boolean
}
export function DecodedAbiMethodArguments({ arguments: argumentsProp, multiLine }: Props) {
export function DecodedAbiMethodArguments({ arguments: argumentsProp, multiline }: Props) {
const renderArgumentValue = useCallback((argument: AbiMethodArgument) => {
if (argument.type === AbiType.Transaction) {
return (
Expand Down Expand Up @@ -52,7 +52,7 @@ export function DecodedAbiMethodArguments({ arguments: argumentsProp, multiLine
[argumentsProp, renderArgumentValue]
)

if (multiLine) {
if (multiline) {
return (
<ul className="pl-4">
{components.map((component, index, array) => (
Expand Down
2 changes: 1 addition & 1 deletion src/features/abi-methods/components/decoded-abi-method.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function DecodedAbiMethod({ abiMethod }: Props) {
<div className="max-h-[450px] overflow-x-auto">
<div>
<span>{abiMethod.name}(</span>
<DecodedAbiMethodArguments arguments={abiMethod.arguments} multiLine={abiMethod.multiline} />)
<DecodedAbiMethodArguments arguments={abiMethod.arguments} multiline={abiMethod.multiline} />)
</div>
<div className="mt-4">
<span>Returns: </span>
Expand Down
96 changes: 77 additions & 19 deletions src/features/abi-methods/data/abi-method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { invariant } from '@/utils/invariant'
import { isArc32AppSpec, isArc4AppSpec } from '@/features/common/utils'
import { createAppInterfaceAtom } from '@/features/app-interfaces/data'
import { sum } from '@/utils/sum'
import { Hint, Struct } from '@/features/app-interfaces/data/types/arc-32/application'

const MAX_LINE_LENGTH = 20

Expand All @@ -21,25 +22,25 @@ export const abiMethodResolver = (transaction: TransactionResult): Atom<Promise<
return undefined
}

const abiMethod = await get(createAbiMethodAtom(transaction))
if (!abiMethod) return undefined
const abiMethodWithHint = await get(createAbiMethodWithHintAtom(transaction))
if (!abiMethodWithHint) return undefined

const methodArguments = await get(createMethodArgumentsAtom(transaction, abiMethod))
const methodReturn = getMethodReturn(transaction, abiMethod)
const methodArguments = await get(createMethodArgumentsAtom(transaction, abiMethodWithHint))
const methodReturn = getMethodReturn(transaction, abiMethodWithHint)

const multiline =
methodArguments.some((argument) => argument.multiline) || sum(methodArguments.map((arg) => arg.length)) > MAX_LINE_LENGTH

return {
name: abiMethod.name,
name: abiMethodWithHint.abiMethod.name,
arguments: methodArguments,
return: methodReturn,
multiline,
} satisfies AbiMethod
})
}

const createAbiMethodAtom = (transaction: TransactionResult): Atom<Promise<algosdk.ABIMethod | undefined>> => {
const createAbiMethodWithHintAtom = (transaction: TransactionResult): Atom<Promise<AbiMethodWithHint | undefined>> => {
return atom(async (get) => {
invariant(transaction['application-transaction'], 'application-transaction is not set')

Expand All @@ -56,28 +57,43 @@ const createAbiMethodAtom = (transaction: TransactionResult): Atom<Promise<algos
: isArc4AppSpec(appSpecVersion.appSpec)
? appSpecVersion.appSpec.methods
: undefined
const hints = isArc32AppSpec(appSpecVersion.appSpec) ? appSpecVersion.appSpec.hints : undefined
if (methods) {
const contractMethod = methods.find((m) => {
const abiMethod = new algosdk.ABIMethod(m)
return uint8ArrayToBase64(abiMethod.getSelector()) === transactionArgs[0]
})
if (contractMethod) return new algosdk.ABIMethod(contractMethod)
if (contractMethod) {
const abiMethod = new algosdk.ABIMethod(contractMethod)
const hint = hints?.[abiMethod.getSignature()]
return { abiMethod, hint }
}
}
}
return undefined
})
}

const createMethodArgumentsAtom = (transaction: TransactionResult, abiMethod: algosdk.ABIMethod): Atom<Promise<AbiMethodArgument[]>> => {
const createMethodArgumentsAtom = (
transaction: TransactionResult,
abiMethodWithHint: AbiMethodWithHint
): Atom<Promise<AbiMethodArgument[]>> => {
return atom(async (get) => {
invariant(transaction['application-transaction'], 'application-transaction is not set')
invariant(transaction['application-transaction']?.['application-args'], 'application-transaction application-args is not set')
const { abiMethod, hint } = abiMethodWithHint

const referencedTransactionIds = await get(getReferencedTransactionIdsAtom(transaction, abiMethod))
const abiValues = getAbiValueArgs(transaction, abiMethod)

const abiArguments: AbiMethodArgument[] = abiMethod.args.map((argumentSpec, index) => {
const argName = argumentSpec.name ?? `arg${index}`
const argHint =
hint && argumentSpec.name && (hint.structs?.[argumentSpec.name] || hint.default_arguments?.[argumentSpec.name])
? ({
struct: hint.structs?.[argumentSpec.name],
} satisfies AbiValueHint)
: undefined

if (algosdk.abiTypeIsTransaction(argumentSpec.type)) {
const transactionId = referencedTransactionIds.shift()!
Expand Down Expand Up @@ -136,26 +152,35 @@ const createMethodArgumentsAtom = (transaction: TransactionResult, abiMethod: al

return {
name: argName,
...getAbiValue(argumentSpec.type, abiValue),
...getAbiValue(argumentSpec.type, abiValue, argHint),
}
})

return abiArguments
})
}

const getMethodReturn = (transaction: TransactionResult, abiMethod: algosdk.ABIMethod): AbiMethodReturn => {
const getMethodReturn = (transaction: TransactionResult, abiMethodWithHint: AbiMethodWithHint): AbiMethodReturn => {
const { abiMethod, hint } = abiMethodWithHint

if (abiMethod.returns.type === 'void') return 'void'
invariant(transaction.logs && transaction.logs.length > 0, 'transaction logs is not set')

const returnHint =
hint && hint.structs?.['output']
? ({
struct: hint.structs?.['output'],
} satisfies AbiValueHint)
: undefined

const abiType = algosdk.ABIType.from(abiMethod.returns.type.toString())
// The first 4 bytes are SHA512_256 hash of the string "return"
const bytes = base64ToBytes(transaction.logs.slice(-1)[0]).subarray(4)
const abiValue = abiType.decode(bytes)
return getAbiValue(abiType, abiValue)
return getAbiValue(abiType, abiValue, returnHint)
}

const getAbiValue = (abiType: algosdk.ABIType, abiValue: algosdk.ABIValue): AbiValue => {
const getAbiValue = (abiType: algosdk.ABIType, abiValue: algosdk.ABIValue, hint?: AbiValueHint): AbiValue => {
if (abiType instanceof algosdk.ABITupleType) {
const childTypes = abiType.childTypes
const abiValues = abiValue as algosdk.ABIValue[]
Expand All @@ -164,14 +189,38 @@ const getAbiValue = (abiType: algosdk.ABIType, abiValue: algosdk.ABIValue): AbiV
}

const childrenValues = abiValues.map((abiValue, index) => getAbiValue(childTypes[index], abiValue))
const length = sum(childrenValues.map((v) => v.length))
const multiline = childrenValues.some((v) => v.multiline) || length > MAX_LINE_LENGTH

return {
type: AbiType.Tuple,
values: childrenValues,
multiline,
length,
if (hint?.struct) {
const values = childTypes.map(
(_, index) => {
const value = childrenValues[index]
const name = hint.struct!.elements[index][0]
return {
name: name,
value: value,
}
},
[{} as Record<string, AbiValue>]
)
const length = sum(values.map((v) => `${v.name}: ${v.value}`.length))
const multiline = values.some((v) => v.value.multiline) || length > MAX_LINE_LENGTH

return {
type: AbiType.Struct,
values: values,
multiline: multiline,
length: length,
}
} else {
const length = sum(childrenValues.map((v) => v.length))
const multiline = childrenValues.some((v) => v.multiline) || length > MAX_LINE_LENGTH

return {
type: AbiType.Tuple,
values: childrenValues,
multiline,
length,
}
}
}
if (abiType instanceof algosdk.ABIArrayStaticType || abiType instanceof algosdk.ABIArrayDynamicType) {
Expand Down Expand Up @@ -335,3 +384,12 @@ const bigintToString = (value: bigint, decimalScale: number): string => {
const fractionString = valueString.slice(valueString.length - decimalScale)
return `${numberString}.${fractionString}`
}

type AbiValueHint = {
struct?: Struct
}

type AbiMethodWithHint = {
abiMethod: algosdk.ABIMethod
hint?: Hint
}
7 changes: 7 additions & 0 deletions src/features/abi-methods/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum AbiType {
Transaction = 'Transaction',
Application = 'Application',
Asset = 'Asset',
Struct = 'Struct',
}

type RepresentationProps = {
Expand All @@ -19,6 +20,11 @@ type RepresentationProps = {
}
export type AbiTupleValue = { type: AbiType.Tuple; values: AbiValue[] } & RepresentationProps
export type AbiArrayValue = { type: AbiType.Array; values: AbiValue[] } & RepresentationProps
export type AbiStructElementValue = {
name: string
value: AbiValue
}
export type AbiStructValue = { type: AbiType.Struct; values: AbiStructElementValue[] } & RepresentationProps
export type AbiValue =
| ({
type: AbiType.String
Expand Down Expand Up @@ -46,6 +52,7 @@ export type AbiValue =
} & RepresentationProps)
| AbiArrayValue
| AbiTupleValue
| AbiStructValue

export type AbiReferenceValue =
| ({ type: AbiType.Account; value: string } & RepresentationProps)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ function Method<TSchema extends z.ZodSchema>({ applicationId, method, appSpec, r
addr: activeAddress,
signer,
},
sendParams: {
populateAppCallResources: true,
},
})

const sentTxns = asTransactionFromSendResult(result)
Expand Down
Loading