Skip to content

Commit 81833af

Browse files
committed
feat: implement method signatures and method selectors
1 parent ac2437b commit 81833af

File tree

8 files changed

+142
-54
lines changed

8 files changed

+142
-54
lines changed

src/abi-metadata.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { BaseContract, Contract } from '@algorandfoundation/algorand-typescript'
22
import { AbiMethodConfig, BareMethodConfig, CreateOptions, OnCompleteActionStr } from '@algorandfoundation/algorand-typescript/arc4'
3+
import { ABIMethod } from 'algosdk'
4+
import { TypeInfo } from './encoders'
5+
import { getArc4TypeName as getArc4TypeNameForARC4Encoded } from './impl/encoded-types'
36
import { DeliberateAny } from './typescript-helpers'
47

58
export interface AbiMetadata {
69
methodName: string
7-
methodSelector: string
10+
methodSignature: string | undefined
811
argTypes: string[]
912
returnType: string
1013
onCreate?: CreateOptions
@@ -28,21 +31,11 @@ export const captureMethodConfig = <T extends Contract>(
2831
methodName: string,
2932
config?: AbiMethodConfig<T> | BareMethodConfig,
3033
): void => {
31-
const metadata = ensureMetadata(contract, methodName)
34+
const metadata = getAbiMetadata(contract, methodName)
3235
metadata.onCreate = config?.onCreate ?? 'disallow'
3336
metadata.allowActions = ([] as OnCompleteActionStr[]).concat(config?.allowActions ?? 'NoOp')
3437
}
3538

36-
const ensureMetadata = <T extends Contract>(contract: T, methodName: string): AbiMetadata => {
37-
if (!hasAbiMetadata(contract)) {
38-
const contractClass = contract.constructor as { new (): T }
39-
Object.getOwnPropertyNames(Object.getPrototypeOf(contract)).forEach((name) => {
40-
attachAbiMetadata(contractClass, name, { methodName: name, methodSelector: name, argTypes: [], returnType: '' })
41-
})
42-
}
43-
return getAbiMetadata(contract, methodName)
44-
}
45-
4639
export const hasAbiMetadata = <T extends Contract>(contract: T): boolean => {
4740
const contractClass = contract.constructor as { new (): T }
4841
return (
@@ -58,3 +51,48 @@ export const getAbiMetadata = <T extends BaseContract>(contract: T, methodName:
5851
) as Record<string, AbiMetadata>
5952
return metadatas[methodName]
6053
}
54+
55+
export const getArc4Signature = (metadata: AbiMetadata): string => {
56+
if (metadata.methodSignature === undefined) {
57+
const argTypes = metadata.argTypes.map((t) => JSON.parse(t) as TypeInfo).map(getArc4TypeName)
58+
const returnType = getArc4TypeName(JSON.parse(metadata.returnType) as TypeInfo)
59+
const method = new ABIMethod({ name: metadata.methodName, args: argTypes.map((t) => ({ type: t })), returns: { type: returnType } })
60+
metadata.methodSignature = method.getSignature()
61+
}
62+
return metadata.methodSignature
63+
}
64+
65+
const getArc4TypeName = (t: TypeInfo): string => {
66+
const map: Record<string, string | ((t: TypeInfo) => string)> = {
67+
void: 'void',
68+
account: 'account',
69+
application: 'application',
70+
asset: 'asset',
71+
boolean: 'bool',
72+
biguint: 'uint512',
73+
bytes: 'byte[]',
74+
string: 'string',
75+
uint64: 'uint64',
76+
OnCompleteAction: 'uint64',
77+
TransactionType: 'uint64',
78+
Transaction: 'txn',
79+
PaymentTxn: 'pay',
80+
KeyRegistrationTxn: 'keyreg',
81+
AssetConfigTxn: 'acfg',
82+
AssetTransferTxn: 'axfer',
83+
AssetFreezeTxn: 'afrz',
84+
ApplicationTxn: 'appl',
85+
'Tuple<.*>': (t) =>
86+
`(${Object.values(t.genericArgs as Record<string, TypeInfo>)
87+
.map(getArc4TypeName)
88+
.join(',')})`,
89+
}
90+
const entry = Object.entries(map).find(([k, _]) => new RegExp(`^${k}$`, 'i').test(t.name))?.[1]
91+
if (entry === undefined) {
92+
return getArc4TypeNameForARC4Encoded(t) ?? t.name
93+
}
94+
if (entry instanceof Function) {
95+
return entry(t)
96+
}
97+
return entry
98+
}

src/impl/encoded-types.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export class UintNImpl<N extends BitSize> extends UintN<N> {
8181
static getMaxBitsLength(typeInfo: TypeInfo): BitSize {
8282
return parseInt((typeInfo.genericArgs as TypeInfo[])![0].name, 10) as BitSize
8383
}
84+
85+
static getArc4TypeName = (t: TypeInfo): string => {
86+
return `uint${this.getMaxBitsLength(t)}`
87+
}
8488
}
8589

8690
const regExpNxM = (maxPrecision: number) => new RegExp(`^\\d*\\.?\\d{0,${maxPrecision}}$`)
@@ -144,6 +148,11 @@ export class UFixedNxMImpl<N extends BitSize, M extends number> extends UFixedNx
144148
const genericArgs = typeInfo.genericArgs as uFixedNxMGenericArgs
145149
return parseInt(genericArgs.n.name, 10) as BitSize
146150
}
151+
152+
static getArc4TypeName = (t: TypeInfo): string => {
153+
const genericArgs = t.genericArgs as uFixedNxMGenericArgs
154+
return `ufixed${genericArgs.n.name}x${genericArgs.m.name}`
155+
}
147156
}
148157

149158
export class ByteImpl extends Byte {
@@ -403,6 +412,11 @@ export class StaticArrayImpl<TItem extends ARC4Encoded, TLength extends number>
403412
}
404413
return size
405414
}
415+
416+
static getArc4TypeName = (t: TypeInfo): string => {
417+
const genericArgs = t.genericArgs as StaticArrayGenericArgs
418+
return `${getArc4TypeName(genericArgs.elementType)}[${genericArgs.size.name}]`
419+
}
406420
}
407421

408422
export class AddressImpl extends Address {
@@ -562,6 +576,11 @@ export class DynamicArrayImpl<TItem extends ARC4Encoded> extends DynamicArray<TI
562576
return result
563577
}
564578

579+
static getArc4TypeName = (t: TypeInfo): string => {
580+
const genericArgs = t.genericArgs as DynamicArrayGenericArgs
581+
return `${getArc4TypeName(genericArgs.elementType)}[]`
582+
}
583+
565584
private encodeWithLength(items: TItem[]) {
566585
return conactUint8Arrays(encodeLength(items.length).asUint8Array(), encode(items))
567586
}
@@ -657,6 +676,11 @@ export class TupleImpl<TTuple extends [ARC4Encoded, ...ARC4Encoded[]]> extends T
657676
}
658677
return size
659678
}
679+
680+
static getArc4TypeName = (t: TypeInfo): string => {
681+
const genericArgs = Object.values(t.genericArgs as Record<string, TypeInfo>)
682+
return `(${genericArgs.map(getArc4TypeName).join(',')})`
683+
}
660684
}
661685

662686
type StructConstraint = Record<string, ARC4Encoded>
@@ -732,6 +756,11 @@ export class StructImpl<T extends StructConstraint> extends (Struct<StructConstr
732756
result.uint8ArrayValue = asUint8Array(bytesValue)
733757
return result
734758
}
759+
760+
static getArc4TypeName = (t: TypeInfo): string => {
761+
const genericArgs = Object.values(t.genericArgs as Record<string, TypeInfo>)
762+
return `(${genericArgs.map(getArc4TypeName).join(',')})`
763+
}
735764
}
736765

737766
export class DynamicBytesImpl extends DynamicBytes {
@@ -783,6 +812,10 @@ export class DynamicBytesImpl extends DynamicBytes {
783812
result.value = dynamicArrayValue
784813
return result
785814
}
815+
816+
static getArc4TypeName = (_t: TypeInfo): string => {
817+
return 'byte[]'
818+
}
786819
}
787820

788821
export class StaticBytesImpl extends StaticBytes {
@@ -838,6 +871,11 @@ export class StaticBytesImpl extends StaticBytes {
838871
static getMaxBytesLength(typeInfo: TypeInfo): number {
839872
return StaticArrayImpl.getMaxBytesLength(typeInfo)
840873
}
874+
875+
static getArc4TypeName = (t: TypeInfo): string => {
876+
const genericArgs = t.genericArgs as StaticArrayGenericArgs
877+
return `byte[${genericArgs.size.name}]`
878+
}
841879
}
842880

843881
const decode = (value: Uint8Array, childTypes: TypeInfo[]) => {
@@ -1097,3 +1135,24 @@ export const getArc4Encoder = <T>(typeInfo: TypeInfo, encoders?: Record<string,
10971135
}
10981136
return encoder as fromBytes<T>
10991137
}
1138+
1139+
export const getArc4TypeName = (typeInfo: TypeInfo): string | undefined => {
1140+
const map = {
1141+
Address: 'address',
1142+
Bool: 'bool',
1143+
Byte: 'byte',
1144+
Str: 'string',
1145+
'UintN<.*>': UintNImpl.getArc4TypeName,
1146+
'UFixedNxM<.*>': UFixedNxMImpl.getArc4TypeName,
1147+
'StaticArray<.*>': StaticArrayImpl.getArc4TypeName,
1148+
'DynamicArray<.*>': DynamicArrayImpl.getArc4TypeName,
1149+
Tuple: TupleImpl.getArc4TypeName,
1150+
Struct: StructImpl.getArc4TypeName,
1151+
DynamicBytes: DynamicBytesImpl.getArc4TypeName,
1152+
'StaticBytes<.*>': StaticBytesImpl.getArc4TypeName,
1153+
}
1154+
const name = Object.entries(map).find(([k, _]) => new RegExp(`^${k}$`, 'i').test(typeInfo.name))?.[1]
1155+
if (typeof name === 'string') return name
1156+
else if (typeof name === 'function') return name(typeInfo)
1157+
return undefined
1158+
}

src/subcontexts/contract-context.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Account, Application, Asset, BaseContract, Bytes, bytes, Contract, LocalState } from '@algorandfoundation/algorand-typescript'
2-
import { getAbiMetadata } from '../abi-metadata'
2+
import { ABIMethod } from 'algosdk'
3+
import { AbiMetadata, getAbiMetadata, getArc4Signature } from '../abi-metadata'
34
import { BytesMap } from '../collections/custom-key-map'
45
import { lazyContext } from '../context-helpers/internal-context'
56
import type { TypeInfo } from '../encoders'
@@ -69,7 +70,7 @@ const extractStates = (contract: BaseContract): States => {
6970
return states
7071
}
7172

72-
const extractArraysFromArgs = (app: Application, args: DeliberateAny[]) => {
73+
const extractArraysFromArgs = (app: Application, methodSelector: Uint8Array, args: DeliberateAny[]) => {
7374
const transactions: Transaction[] = []
7475
const accounts: Account[] = []
7576
const apps: Application[] = [app]
@@ -90,9 +91,7 @@ const extractArraysFromArgs = (app: Application, args: DeliberateAny[]) => {
9091
assets.push(arg as Asset)
9192
}
9293
}
93-
94-
// TODO: use actual method selector in appArgs
95-
return { accounts, apps, assets, transactions, appArgs: [Bytes('method_selector'), ...appArgs] }
94+
return { accounts, apps, assets, transactions, appArgs: [Bytes(methodSelector), ...appArgs] }
9695
}
9796

9897
function isTransaction(obj: unknown): obj is Transaction {
@@ -112,14 +111,14 @@ export class ContractContext {
112111
return new proxy(...args)
113112
}
114113

115-
static createMethodCallTxns<TParams extends unknown[], TReturn>(
114+
static createMethodCallTxns<TParams extends unknown[]>(
116115
contract: BaseContract,
117-
method: (...args: TParams) => TReturn,
116+
abiMetadata: AbiMetadata | undefined,
118117
...args: TParams
119118
): Transaction[] {
120-
const abiMetadata = getAbiMetadata(contract, method.name)
121119
const app = lazyContext.ledger.getApplicationForContract(contract)
122-
const { transactions, ...appCallArgs } = extractArraysFromArgs(app, args)
120+
const methodSelector = abiMetadata ? ABIMethod.fromSignature(getArc4Signature(abiMetadata)).getSelector() : new Uint8Array()
121+
const { transactions, ...appCallArgs } = extractArraysFromArgs(app, methodSelector, args)
123122
const appTxn = lazyContext.any.txn.applicationCall({
124123
appId: app,
125124
...appCallArgs,
@@ -171,7 +170,7 @@ export class ContractContext {
171170
const isAbiMethod = isArc4 && abiMetadata
172171
if (isAbiMethod || isProgramMethod) {
173172
return (...args: DeliberateAny[]): DeliberateAny => {
174-
const txns = ContractContext.createMethodCallTxns(receiver, orig as DeliberateAny, ...args)
173+
const txns = ContractContext.createMethodCallTxns(receiver, abiMetadata, ...args)
175174
return lazyContext.txn.ensureScope(txns).execute(() => {
176175
const returnValue = (orig as DeliberateAny).apply(target, args)
177176
if (!isProgramMethod && isAbiMethod && returnValue !== undefined) {

src/subcontexts/transaction-context.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { bytes, Contract, internal, TransactionType, uint64 } from '@algorandfoundation/algorand-typescript'
22
import algosdk from 'algosdk'
3+
import { getAbiMetadata } from '../abi-metadata'
34
import { lazyContext } from '../context-helpers/internal-context'
45
import { DecodedLogs, decodeLogs, LogDecoding } from '../decode-logs'
56
import { testInvariant } from '../errors'
@@ -130,7 +131,8 @@ export class TransactionContext {
130131
...args: TParams
131132
): DeferredAppCall<TParams, TReturn> {
132133
const appId = lazyContext.ledger.getApplicationForContract(contract)
133-
const txns = ContractContext.createMethodCallTxns(contract, method, ...args)
134+
const abiMetadata = getAbiMetadata(contract, method.name)
135+
const txns = ContractContext.createMethodCallTxns(contract, abiMetadata, ...args)
134136
return new DeferredAppCall(appId.id, txns, method, args)
135137
}
136138

src/test-transformer/node-factory.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,21 @@ export const nodeFactory = {
4848
)
4949
},
5050

51-
attachMetaData(classIdentifier: ts.Identifier, method: ts.MethodDeclaration, functionType: ptypes.FunctionPType) {
51+
attachMetaData(
52+
classIdentifier: ts.Identifier,
53+
method: ts.MethodDeclaration,
54+
functionType: ptypes.FunctionPType,
55+
argTypes: string[],
56+
returnType: string,
57+
) {
5258
const methodName = getPropertyNameAsString(method.name)
5359
const metadata = factory.createObjectLiteralExpression([
5460
factory.createPropertyAssignment('methodName', methodName),
55-
factory.createPropertyAssignment('methodSelector', methodName),
5661
factory.createPropertyAssignment(
5762
'argTypes',
58-
factory.createArrayLiteralExpression(functionType.parameters.map((p) => factory.createStringLiteral(p[1].fullName))),
63+
factory.createArrayLiteralExpression(argTypes.map((p) => factory.createStringLiteral(p))),
5964
),
60-
factory.createPropertyAssignment('returnType', factory.createStringLiteral(functionType.returnType.fullName)),
65+
factory.createPropertyAssignment('returnType', factory.createStringLiteral(returnType)),
6166
])
6267
return factory.createExpressionStatement(
6368
factory.createCallExpression(

src/test-transformer/visitors.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,9 @@ class ClassVisitor {
249249
if (this.classDec.name && this.isArc4) {
250250
const methodType = this.helper.resolveType(node)
251251
if (methodType instanceof ptypes.FunctionPType) {
252-
this.helper.additionalStatements.push(nodeFactory.attachMetaData(this.classDec.name, node, methodType))
252+
const argTypes = methodType.parameters.map((p) => JSON.stringify(getGenericTypeInfo(p[1])))
253+
const returnType = JSON.stringify(getGenericTypeInfo(methodType.returnType))
254+
this.helper.additionalStatements.push(nodeFactory.attachMetaData(this.classDec.name, node, methodType, argTypes, returnType))
253255
}
254256
}
255257

@@ -318,7 +320,7 @@ const getGenericTypeInfo = (type: ptypes.PType): TypeInfo => {
318320
.map(([key, value]) => [key, getGenericTypeInfo(value)])
319321
.filter((x) => !!x),
320322
)
321-
} else if (type instanceof ptypes.ARC4TupleType) {
323+
} else if (type instanceof ptypes.ARC4TupleType || type instanceof ptypes.TuplePType) {
322324
genericArgs.push(...type.items.map(getGenericTypeInfo))
323325
}
324326

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { arc4, assert, Bytes, gtxn, op, uint64 } from '@algorandfoundation/algorand-typescript'
1+
import { arc4, assert, Global, gtxn, op, uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { interpretAsArc4, methodSelector, UintN64 } from '@algorandfoundation/algorand-typescript/arc4'
23

34
export class AppExpectingEffects extends arc4.Contract {
45
@arc4.abimethod()
@@ -13,16 +14,8 @@ export class AppExpectingEffects extends arc4.Contract {
1314

1415
@arc4.abimethod()
1516
public log_group(appCall: gtxn.ApplicationTxn): void {
16-
assert(appCall.appArgs(0) === Bytes('some_value()uint64'), 'expected correct method called')
17+
assert(appCall.appArgs(0) === methodSelector('some_value()uint64'), 'expected correct method called')
1718
assert(appCall.numLogs === 1, 'expected logs')
18-
assert(appCall.lastLog === Bytes('this is a log statement'))
19+
assert(interpretAsArc4<UintN64>(appCall.lastLog, 'log').native === (appCall.groupIndex + 1) * Global.groupSize)
1920
}
20-
21-
// TODO: uncomment when arc4 stubs are implemented
22-
// @arc4.abimethod()
23-
// public log_group(appCall: gtxn.ApplicationTxn): void {
24-
// assert(appCall.appArgs(0) === arc4.arc4Signature("some_value()uint64"), "expected correct method called")
25-
// assert(appCall.numLogs === 1, "expected logs")
26-
// assert(arc4.UInt64.from_log(appCall.lastLog) === (appCall.groupIndex + 1) * Global.groupSize)
27-
// }
2821
}

tests/state-op-codes.spec.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount'
22
import { ApplicationClient } from '@algorandfoundation/algokit-utils/types/app-client'
33
import { AppSpec } from '@algorandfoundation/algokit-utils/types/app-spec'
44
import { Account, arc4, bytes, Bytes, internal, op, TransactionType, uint64, Uint64 } from '@algorandfoundation/algorand-typescript'
5-
import { DynamicBytes } from '@algorandfoundation/algorand-typescript/arc4'
5+
import { DynamicBytes, UintN64 } from '@algorandfoundation/algorand-typescript/arc4'
66
import { afterEach, describe, expect, it, test } from 'vitest'
77
import { TestExecutionContext } from '../src'
8-
import { MIN_TXN_FEE, ZERO_ADDRESS } from '../src/constants'
8+
import { ABI_RETURN_VALUE_LOG_PREFIX, MIN_TXN_FEE, ZERO_ADDRESS } from '../src/constants'
99
import { testInvariant } from '../src/errors'
1010
import { Block, gloadBytes, gloadUint64 } from '../src/impl'
1111
import { AccountCls } from '../src/impl/account'
@@ -285,22 +285,12 @@ describe('State op codes', async () => {
285285

286286
it('should be able to pass app call txn as app arg', async () => {
287287
const appCallTxn = ctx.any.txn.applicationCall({
288-
appArgs: [Bytes('some_value()uint64')],
289-
appLogs: [Bytes('this is a log statement')],
288+
appArgs: [arc4.methodSelector('some_value()uint64')],
289+
appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(new UintN64(2).bytes)],
290290
})
291291
const contract = ctx.contract.create(AppExpectingEffects)
292292
contract.log_group(appCallTxn)
293293
})
294-
295-
// TODO: uncomment when arc4 stubs are implemented
296-
// it('should be able to pass app call txn as app arg', async () => {
297-
// const appCallTxn = ctx.any.txn.applicationCall({
298-
// appArgs: [arc4.arc4Signature("some_value()uint64")],
299-
// logs: [arc4Prefix.concat(arc4.Uint64(2).bytes)]
300-
// })
301-
// const contract = ctx.contract.create(AppExpectingEffects)
302-
// contract.log_group(appCallTxn)
303-
// })
304294
})
305295

306296
describe('itxn', async () => {

0 commit comments

Comments
 (0)