Skip to content

Commit 37bdf71

Browse files
committed
- fix: capture method config at the earlier stage instead of on instantiation
as the contract might not be instantiated in the case of contract method being passed as a parameter to `methodSelector` function; e.g. `methodSelector(HelloWorld.prototype.greet)` - fix: prevent sub classes of the same parent from overwritting the config of other siblings
1 parent 6e27f77 commit 37bdf71

File tree

9 files changed

+84
-51
lines changed

9 files changed

+84
-51
lines changed

examples/precompiled/contract.algo.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { Hello, HelloTemplate, HelloTemplateCustomPrefix, LargeProgram, Terrible
66
export class HelloFactory extends Contract {
77
test_compile_contract() {
88
const compiled = compile(Hello)
9-
109
const helloApp = itxn
1110
.applicationCall({
1211
appArgs: [methodSelector(Hello.prototype.create), encodeArc4('hello')],

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@
7171
"tslib": "^2.6.2"
7272
},
7373
"dependencies": {
74-
"@algorandfoundation/algorand-typescript": "^1.0.0-beta.18",
75-
"@algorandfoundation/puya-ts": "^1.0.0-beta.25",
74+
"@algorandfoundation/algorand-typescript": "^1.0.0-beta.19",
75+
"@algorandfoundation/puya-ts": "^1.0.0-beta.26",
7676
"elliptic": "^6.5.7",
7777
"js-sha256": "^0.11.0",
7878
"js-sha3": "^0.9.3",

src/abi-metadata.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { BaseContract, Contract } from '@algorandfoundation/algorand-typescript'
1+
import type { Contract } from '@algorandfoundation/algorand-typescript'
22
import type { AbiMethodConfig, BareMethodConfig, CreateOptions, OnCompleteActionStr } from '@algorandfoundation/algorand-typescript/arc4'
33
import js_sha512 from 'js-sha512'
44
import type { TypeInfo } from './encoders'
@@ -7,28 +7,41 @@ import type { DeliberateAny } from './typescript-helpers'
77

88
export interface AbiMetadata {
99
methodName: string
10-
methodNameOverride?: string
10+
name?: string
1111
methodSignature: string | undefined
1212
argTypes: string[]
1313
returnType: string
1414
onCreate?: CreateOptions
1515
allowActions?: OnCompleteActionStr[]
1616
}
1717
const AbiMetaSymbol = Symbol('AbiMetadata')
18+
const overwrittenMetadata: Array<readonly [Contract | { new (): Contract }, Record<string, AbiMetadata>]> = []
1819
export const isContractProxy = Symbol('isContractProxy')
1920
export const attachAbiMetadata = (contract: { new (): Contract }, methodName: string, metadata: AbiMetadata): void => {
2021
const metadatas: Record<string, AbiMetadata> = (AbiMetaSymbol in contract ? contract[AbiMetaSymbol] : {}) as Record<string, AbiMetadata>
21-
metadatas[methodName] = metadata
22+
23+
// classes inherited from the same parent shares the same metadata object stored in `AbiMetaSymbol`
24+
// to prevent one subclass from overwriting the metadata of another subclass, we store the overwritten metadata in a separate array
25+
if (metadatas[methodName]) {
26+
let contractMetadatas = overwrittenMetadata.find(([c]) => c === contract)
27+
if (!contractMetadatas) {
28+
contractMetadatas = [contract, {}]
29+
overwrittenMetadata.push(contractMetadatas)
30+
}
31+
contractMetadatas[1][methodName] = metadata
32+
} else {
33+
metadatas[methodName] = metadata
34+
}
2235
if (!(AbiMetaSymbol in contract)) {
2336
Object.defineProperty(contract, AbiMetaSymbol, {
2437
value: metadatas,
2538
writable: true,
26-
enumerable: false,
39+
enumerable: true,
2740
})
2841
}
2942
}
3043

31-
export const copyAbiMetadatas = <T extends BaseContract>(sourceContract: T, targetContract: T): void => {
44+
export const copyAbiMetadatas = <T extends Contract>(sourceContract: T, targetContract: T): void => {
3245
const metadatas = getContractAbiMetadata(sourceContract)
3346
Object.defineProperty(targetContract, AbiMetaSymbol, {
3447
value: metadatas,
@@ -43,8 +56,7 @@ export const captureMethodConfig = <T extends Contract>(
4356
config?: AbiMethodConfig<T> | BareMethodConfig,
4457
): void => {
4558
const metadata = getContractMethodAbiMetadata(contract, methodName)
46-
47-
metadata.methodNameOverride = config && 'name' in config ? config.name : undefined
59+
metadata.name = (config as AbiMethodConfig<T>)?.name ?? methodName
4860
metadata.onCreate = config?.onCreate ?? 'disallow'
4961
metadata.allowActions = ([] as OnCompleteActionStr[]).concat(config?.allowActions ?? 'NoOp')
5062
}
@@ -55,19 +67,20 @@ export const hasAbiMetadata = <T extends Contract>(contract: T): boolean => {
5567
Object.getOwnPropertySymbols(contractClass).some((s) => s.toString() === AbiMetaSymbol.toString()) || AbiMetaSymbol in contractClass
5668
)
5769
}
58-
export const getContractAbiMetadata = <T extends BaseContract>(contract: T): Record<string, AbiMetadata> => {
70+
export const getContractAbiMetadata = <T extends Contract>(contract: T): Record<string, AbiMetadata> => {
71+
const overwrittenMetadataEntry = overwrittenMetadata.find(([c]) => c === contract)
5972
if ((contract as DeliberateAny)[AbiMetaSymbol]) {
60-
return (contract as DeliberateAny)[AbiMetaSymbol] as Record<string, AbiMetadata>
73+
return { ...((contract as DeliberateAny)[AbiMetaSymbol] as Record<string, AbiMetadata>), ...overwrittenMetadataEntry?.[1] }
6174
}
6275
const contractClass = contract.constructor as { new (): T }
6376
const s = Object.getOwnPropertySymbols(contractClass).find((s) => s.toString() === AbiMetaSymbol.toString())
6477
const metadatas: Record<string, AbiMetadata> = (
6578
s ? (contractClass as DeliberateAny)[s] : AbiMetaSymbol in contractClass ? contractClass[AbiMetaSymbol] : {}
6679
) as Record<string, AbiMetadata>
67-
return metadatas
80+
return { ...metadatas, ...overwrittenMetadataEntry?.[1] }
6881
}
6982

70-
export const getContractMethodAbiMetadata = <T extends BaseContract>(contract: T, methodName: string): AbiMetadata => {
83+
export const getContractMethodAbiMetadata = <T extends Contract>(contract: T, methodName: string): AbiMetadata => {
7184
const metadatas = getContractAbiMetadata(contract)
7285
return metadatas[methodName]
7386
}
@@ -76,7 +89,7 @@ export const getArc4Signature = (metadata: AbiMetadata): string => {
7689
if (metadata.methodSignature === undefined) {
7790
const argTypes = metadata.argTypes.map((t) => JSON.parse(t) as TypeInfo).map(getArc4TypeName)
7891
const returnType = getArc4TypeName(JSON.parse(metadata.returnType) as TypeInfo)
79-
metadata.methodSignature = `${metadata.methodNameOverride ?? metadata.methodName}(${argTypes.join(',')})${returnType}`
92+
metadata.methodSignature = `${metadata.name ?? metadata.methodName}(${argTypes.join(',')})${returnType}`
8093
}
8194
return metadata.methodSignature
8295
}

src/impl/contract.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { arc4, bytes } from '@algorandfoundation/algorand-typescript'
22
import { encodingUtil } from '@algorandfoundation/puya-ts'
3-
import { captureMethodConfig, getArc4Selector, getContractMethodAbiMetadata } from '../abi-metadata'
3+
import { getArc4Selector, getContractMethodAbiMetadata } from '../abi-metadata'
44
import type { DeliberateAny } from '../typescript-helpers'
55
import { BaseContract } from './base-contract'
66
import { sha512_256 } from './crypto'
@@ -14,35 +14,30 @@ export class Contract extends BaseContract {
1414
}
1515
}
1616

17-
export function abimethod<TContract extends Contract>(config?: arc4.AbiMethodConfig<TContract>) {
17+
export function abimethod<TContract extends Contract>(_config?: arc4.AbiMethodConfig<TContract>) {
1818
return function <TArgs extends DeliberateAny[], TReturn>(
1919
target: (this: TContract, ...args: TArgs) => TReturn,
20-
ctx: ClassMethodDecoratorContext<TContract>,
2120
): (this: TContract, ...args: TArgs) => TReturn {
22-
ctx.addInitializer(function () {
23-
captureMethodConfig(this, target.name, config)
24-
})
2521
return target
2622
}
2723
}
2824

29-
export function baremethod<TContract extends Contract>(config?: arc4.BareMethodConfig) {
25+
export function baremethod<TContract extends Contract>(_config?: arc4.BareMethodConfig) {
3026
return function <TArgs extends DeliberateAny[], TReturn>(
3127
target: (this: TContract, ...args: TArgs) => TReturn,
32-
ctx: ClassMethodDecoratorContext<TContract>,
3328
): (this: TContract, ...args: TArgs) => TReturn {
34-
ctx.addInitializer(function () {
35-
captureMethodConfig(this, target.name, config)
36-
})
3729
return target
3830
}
3931
}
4032

41-
export const methodSelector = (methodSignature: Parameters<typeof arc4.methodSelector>[0], contract?: DeliberateAny): bytes => {
33+
export const methodSelector = <TContract extends Contract>(
34+
methodSignature: Parameters<typeof arc4.methodSelector>[0],
35+
contract?: TContract,
36+
): bytes => {
4237
if (typeof methodSignature === 'string') {
4338
return sha512_256(Bytes(encodingUtil.utf8ToUint8Array(methodSignature))).slice(0, 4)
4439
} else {
45-
const abiMetadata = getContractMethodAbiMetadata(contract, methodSignature.name)
40+
const abiMetadata = getContractMethodAbiMetadata(contract!, methodSignature.name)
4641
return Bytes(getArc4Selector(abiMetadata))
4742
}
4843
}

src/runtime-helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { AccountCls } from './impl/reference'
99
import type { DeliberateAny } from './typescript-helpers'
1010
import { nameOfType } from './util'
1111

12-
export { attachAbiMetadata } from './abi-metadata'
12+
export { attachAbiMetadata, captureMethodConfig } from './abi-metadata'
1313
export { emitImpl } from './impl/emit'
1414
export * from './impl/encoded-types'
1515
export { decodeArc4Impl, encodeArc4Impl } from './impl/encoded-types'

src/subcontexts/contract-context.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,14 +213,14 @@ export class ContractContext {
213213
lazyContext.txn.ensureScope([txn]).execute(() => {
214214
t = new target(...args)
215215
})
216-
appData.isCreating = hasCreateMethods(t!)
216+
appData.isCreating = isArc4 && hasCreateMethods(t! as Contract)
217217
const instance = new Proxy(t!, {
218218
get(target, prop, receiver) {
219219
if (prop === isContractProxy) {
220220
return true
221221
}
222222
const orig = Reflect.get(target, prop, receiver)
223-
const abiMetadata = getContractMethodAbiMetadata(target, prop as string)
223+
const abiMetadata = isArc4 ? getContractMethodAbiMetadata(target as Contract, prop as string) : undefined
224224
const isProgramMethod = prop === 'approvalProgram' || prop === 'clearStateProgram'
225225
const isAbiMethod = isArc4 && abiMetadata
226226
if (isAbiMethod || isProgramMethod) {
@@ -245,7 +245,9 @@ export class ContractContext {
245245

246246
onConstructed(application, instance, getContractOptions(t!))
247247

248-
copyAbiMetadatas(t!, instance)
248+
if (isArc4) {
249+
copyAbiMetadatas(t! as Contract, instance as Contract)
250+
}
249251

250252
return instance
251253
},
@@ -258,7 +260,7 @@ const getContractOptions = (contract: BaseContract): ContractOptionsParameter |
258260
return contractClass[ContractOptionsSymbol] as ContractOptionsParameter
259261
}
260262

261-
const hasCreateMethods = (contract: BaseContract) => {
263+
const hasCreateMethods = (contract: Contract) => {
262264
const metadatas = getContractAbiMetadata(contract)
263265
return Object.values(metadatas).some((metadata) => (metadata.onCreate ?? 'disallow') !== 'disallow')
264266
}

src/test-transformer/node-factory.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ export const nodeFactory = {
7373
)
7474
},
7575

76+
captureMethodConfig(classIdentifier: ts.Identifier, method: ts.MethodDeclaration, callExpression: ts.CallExpression) {
77+
const methodName = getPropertyNameAsString(method.name)
78+
return factory.createExpressionStatement(
79+
factory.createCallExpression(
80+
factory.createPropertyAccessExpression(factory.createIdentifier('runtimeHelpers'), factory.createIdentifier('captureMethodConfig')),
81+
undefined,
82+
[classIdentifier, methodName, ...callExpression.arguments],
83+
),
84+
)
85+
},
86+
7687
captureGenericTypeInfo(x: ts.Expression, info: string) {
7788
return factory.createCallExpression(
7889
factory.createPropertyAccessExpression(
@@ -119,4 +130,8 @@ export const nodeFactory = {
119130
}
120131
return node
121132
},
133+
134+
callDecoratorMethod(node: ts.CallExpression, className: ts.Identifier) {
135+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [className, ...node.arguments])
136+
},
122137
} satisfies Record<string, (...args: DeliberateAny[]) => ts.Node>

src/test-transformer/visitors.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ class ExpressionVisitor {
131131
private helper: VisitorHelper,
132132
private expressionNode: ts.Expression,
133133
private stubbedFunctionName?: string,
134+
private className?: ts.Identifier,
135+
private methodNode?: ts.MethodDeclaration,
134136
) {}
135137

136138
public result(): ts.Expression {
@@ -162,16 +164,20 @@ class ExpressionVisitor {
162164
let infoArg = info
163165
if (isCallingEmit(stubbedFunctionName)) {
164166
infoArg = this.helper.resolveTypeParameters(updatedNode).map(getGenericTypeInfo)[0]
165-
}
166-
if (isCallingDecodeArc4(stubbedFunctionName)) {
167+
} else if (isCallingDecodeArc4(stubbedFunctionName)) {
167168
const targetType = ptypes.ptypeToArc4EncodedType(type, this.helper.sourceLocation(node))
168169
const targetTypeInfo = getGenericTypeInfo(targetType)
169170
infoArg = targetTypeInfo
171+
} else if (isCallingDecoratorMethod(stubbedFunctionName)) {
172+
this.helper.additionalStatements.push(nodeFactory.captureMethodConfig(this.className!, this.methodNode!, updatedNode))
170173
}
174+
171175
updatedNode = stubbedFunctionName
172176
? isCallingMethodSelector(stubbedFunctionName)
173177
? nodeFactory.callMethodSelectorFunction(updatedNode)
174-
: nodeFactory.callStubbedFunction(stubbedFunctionName, updatedNode, infoArg)
178+
: isCallingDecoratorMethod(stubbedFunctionName)
179+
? updatedNode
180+
: nodeFactory.callStubbedFunction(stubbedFunctionName, updatedNode, infoArg)
175181
: updatedNode
176182
}
177183
return needsToCaptureTypeInfo
@@ -208,7 +214,8 @@ class FunctionOrMethodVisitor {
208214
constructor(
209215
protected context: ts.TransformationContext,
210216
protected helper: VisitorHelper,
211-
private isFunction?: boolean,
217+
protected className?: ts.Identifier,
218+
protected methodNode?: ts.MethodDeclaration,
212219
) {}
213220
protected visit = (node: ts.Node): ts.Node => {
214221
return ts.visitEachChild(this.updateNode(node), this.visit, this.context)
@@ -269,7 +276,7 @@ class FunctionOrMethodVisitor {
269276
if (ts.isCallExpression(node)) {
270277
const stubbedFunctionName = tryGetStubbedFunctionName(node, this.helper)
271278
if (stubbedFunctionName) {
272-
return new ExpressionVisitor(this.context, this.helper, node, stubbedFunctionName).result()
279+
return new ExpressionVisitor(this.context, this.helper, node, stubbedFunctionName, this.className, this.methodNode).result()
273280
}
274281
}
275282

@@ -283,7 +290,7 @@ class FunctionLikeDecVisitor extends FunctionOrMethodVisitor {
283290
helper: VisitorHelper,
284291
private funcNode: ts.SignatureDeclaration,
285292
) {
286-
super(context, helper, true)
293+
super(context, helper)
287294
}
288295

289296
public result(): ts.SignatureDeclaration {
@@ -294,9 +301,10 @@ class MethodDecVisitor extends FunctionOrMethodVisitor {
294301
constructor(
295302
context: ts.TransformationContext,
296303
helper: VisitorHelper,
297-
private methodNode: ts.MethodDeclaration,
304+
methodNode: ts.MethodDeclaration,
305+
className: ts.Identifier | undefined,
298306
) {
299-
super(context, helper)
307+
super(context, helper, className, methodNode)
300308
}
301309

302310
public result(): ts.MethodDeclaration {
@@ -330,7 +338,7 @@ class ClassVisitor {
330338
}
331339
}
332340

333-
return new MethodDecVisitor(this.context, this.helper, node).result()
341+
return new MethodDecVisitor(this.context, this.helper, node, this.classDec.name).result()
334342
}
335343

336344
if (ts.isCallExpression(node)) {
@@ -421,10 +429,11 @@ const tryGetStubbedFunctionName = (node: ts.CallExpression, helper: VisitorHelpe
421429
if (sourceFileName && !algotsModulePaths.some((s) => sourceFileName.includes(s))) return undefined
422430
}
423431
const functionName = functionSymbol?.getName() ?? identityExpression.text
424-
const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'emit', 'methodSelector']
432+
const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'emit', 'methodSelector', 'abimethod', 'baremethod']
425433
return stubbedFunctionNames.includes(functionName) ? functionName : undefined
426434
}
427435

428436
const isCallingDecodeArc4 = (functionName: string | undefined): boolean => ['decodeArc4', 'encodeArc4'].includes(functionName ?? '')
429437
const isCallingEmit = (functionName: string | undefined): boolean => 'emit' === (functionName ?? '')
430438
const isCallingMethodSelector = (functionName: string | undefined): boolean => 'methodSelector' === (functionName ?? '')
439+
const isCallingDecoratorMethod = (functionName: string | undefined): boolean => ['abimethod', 'baremethod'].includes(functionName ?? '')

0 commit comments

Comments
 (0)