Skip to content

Commit 78b3de5

Browse files
committed
refactor: consistently store abi metadata against contract class
1 parent 969d96a commit 78b3de5

File tree

6 files changed

+61
-76
lines changed

6 files changed

+61
-76
lines changed

src/abi-metadata.ts

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import type { Contract } from '@algorandfoundation/algorand-typescript'
21
import type { AbiMethodConfig, BareMethodConfig, CreateOptions, OnCompleteActionStr } from '@algorandfoundation/algorand-typescript/arc4'
32
import js_sha512 from 'js-sha512'
43
import type { TypeInfo } from './encoders'
4+
import { Contract } from './impl/contract'
55
import { getArc4TypeName as getArc4TypeNameForARC4Encoded } from './impl/encoded-types'
6-
import type { DeliberateAny } from './typescript-helpers'
76

87
export interface AbiMetadata {
98
methodName: string
@@ -14,40 +13,15 @@ export interface AbiMetadata {
1413
onCreate?: CreateOptions
1514
allowActions?: OnCompleteActionStr[]
1615
}
17-
const AbiMetaSymbol = Symbol('AbiMetadata')
18-
const overwrittenMetadata: Array<readonly [Contract | { new (): Contract }, Record<string, AbiMetadata>]> = []
16+
export const AbiMetaSymbol = Symbol('AbiMetadata')
1917
export const isContractProxy = Symbol('isContractProxy')
18+
const metadataStore: Map<{ new (): Contract }, Record<string, AbiMetadata>> = new Map()
2019
export const attachAbiMetadata = (contract: { new (): Contract }, methodName: string, metadata: AbiMetadata): void => {
21-
const metadatas: Record<string, AbiMetadata> = (AbiMetaSymbol in contract ? contract[AbiMetaSymbol] : {}) as Record<string, AbiMetadata>
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-
}
35-
if (!(AbiMetaSymbol in contract)) {
36-
Object.defineProperty(contract, AbiMetaSymbol, {
37-
value: metadatas,
38-
writable: true,
39-
enumerable: true,
40-
})
20+
if (!metadataStore.has(contract)) {
21+
metadataStore.set(contract, {})
4122
}
42-
}
43-
44-
export const copyAbiMetadatas = <T extends Contract>(sourceContract: T, targetContract: T): void => {
45-
const metadatas = getContractAbiMetadata(sourceContract)
46-
Object.defineProperty(targetContract, AbiMetaSymbol, {
47-
value: metadatas,
48-
writable: true,
49-
enumerable: false,
50-
})
23+
const metadatas: Record<string, AbiMetadata> = metadataStore.get(contract) as Record<string, AbiMetadata>
24+
metadatas[methodName] = metadata
5125
}
5226

5327
export const captureMethodConfig = <T extends Contract>(
@@ -61,23 +35,33 @@ export const captureMethodConfig = <T extends Contract>(
6135
metadata.allowActions = ([] as OnCompleteActionStr[]).concat(config?.allowActions ?? 'NoOp')
6236
}
6337

64-
export const hasAbiMetadata = <T extends Contract>(contract: T): boolean => {
65-
const contractClass = contract.constructor as { new (): T }
66-
return (
67-
Object.getOwnPropertySymbols(contractClass).some((s) => s.toString() === AbiMetaSymbol.toString()) || AbiMetaSymbol in contractClass
68-
)
69-
}
70-
export const getContractAbiMetadata = <T extends Contract>(contract: T): Record<string, AbiMetadata> => {
71-
const overwrittenMetadataEntry = overwrittenMetadata.find(([c]) => c === contract)
72-
if ((contract as DeliberateAny)[AbiMetaSymbol]) {
73-
return { ...((contract as DeliberateAny)[AbiMetaSymbol] as Record<string, AbiMetadata>), ...overwrittenMetadataEntry?.[1] }
38+
export const getContractAbiMetadata = <T extends Contract>(contract: T | { new (): T }): Record<string, AbiMetadata> => {
39+
// Initialize result object to store merged metadata
40+
const result: Record<string, AbiMetadata> = {}
41+
42+
// Get the contract's class
43+
let currentClass = contract instanceof Contract ? (contract.constructor as { new (): T }) : contract
44+
45+
// Walk up the prototype chain
46+
while (currentClass && currentClass.prototype) {
47+
// Find metadata for current class
48+
const currentMetadata = metadataStore.get(currentClass)
49+
50+
if (currentMetadata) {
51+
// Merge metadata with existing result (don't override existing entries)
52+
const classMetadata = currentMetadata
53+
for (const [methodName, metadata] of Object.entries(classMetadata)) {
54+
if (!(methodName in result)) {
55+
result[methodName] = metadata
56+
}
57+
}
58+
}
59+
60+
// Move up the prototype chain
61+
currentClass = Object.getPrototypeOf(currentClass)
7462
}
75-
const contractClass = contract.constructor as { new (): T }
76-
const s = Object.getOwnPropertySymbols(contractClass).find((s) => s.toString() === AbiMetaSymbol.toString())
77-
const metadatas: Record<string, AbiMetadata> = (
78-
s ? (contractClass as DeliberateAny)[s] : AbiMetaSymbol in contractClass ? contractClass[AbiMetaSymbol] : {}
79-
) as Record<string, AbiMetadata>
80-
return { ...metadatas, ...overwrittenMetadataEntry?.[1] }
63+
64+
return result
8165
}
8266

8367
export const getContractMethodAbiMetadata = <T extends Contract>(contract: T, methodName: string): AbiMetadata => {

src/impl/contract.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import type { arc4, bytes } from '@algorandfoundation/algorand-typescript'
2-
import { encodingUtil } from '@algorandfoundation/puya-ts'
3-
import { getArc4Selector, getContractMethodAbiMetadata } from '../abi-metadata'
1+
import type { arc4 } from '@algorandfoundation/algorand-typescript'
42
import type { DeliberateAny } from '../typescript-helpers'
53
import { BaseContract } from './base-contract'
6-
import { sha512_256 } from './crypto'
7-
import { Bytes } from './primitives'
84

95
export class Contract extends BaseContract {
106
static isArc4 = true
@@ -29,15 +25,3 @@ export function baremethod<TContract extends Contract>(_config?: arc4.BareMethod
2925
return target
3026
}
3127
}
32-
33-
export const methodSelector = <TContract extends Contract>(
34-
methodSignature: Parameters<typeof arc4.methodSelector>[0],
35-
contract?: TContract,
36-
): bytes => {
37-
if (typeof methodSignature === 'string') {
38-
return sha512_256(Bytes(encodingUtil.utf8ToUint8Array(methodSignature))).slice(0, 4)
39-
} else {
40-
const abiMetadata = getContractMethodAbiMetadata(contract!, methodSignature.name)
41-
return Bytes(getArc4Selector(abiMetadata))
42-
}
43-
}

src/impl/method-selector.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { arc4, bytes } from '@algorandfoundation/algorand-typescript'
2+
import { encodingUtil } from '@algorandfoundation/puya-ts'
3+
import { getArc4Selector, getContractMethodAbiMetadata } from '../abi-metadata'
4+
import type { Contract } from './contract'
5+
import { sha512_256 } from './crypto'
6+
import { Bytes } from './primitives'
7+
8+
export const methodSelector = <TContract extends Contract>(
9+
methodSignature: Parameters<typeof arc4.methodSelector>[0],
10+
contract?: TContract,
11+
): bytes => {
12+
if (typeof methodSignature === 'string') {
13+
return sha512_256(Bytes(encodingUtil.utf8ToUint8Array(methodSignature))).slice(0, 4)
14+
} else {
15+
const abiMetadata = getContractMethodAbiMetadata(contract!, methodSignature.name)
16+
return Bytes(getArc4Selector(abiMetadata))
17+
}
18+
}

src/internal/arc4.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from '@algorandfoundation/algorand-typescript/arc4'
2-
export { abimethod, baremethod, Contract, methodSelector } from '../impl/contract'
2+
export { abimethod, baremethod, Contract } from '../impl/contract'
3+
export { methodSelector } from '../impl/method-selector'

src/subcontexts/contract-context.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Account, Application, Asset, contract, LocalState } from '@algorandfoundation/algorand-typescript'
22
import type { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4'
33
import type { AbiMetadata } from '../abi-metadata'
4-
import { copyAbiMetadatas, getArc4Selector, getContractAbiMetadata, getContractMethodAbiMetadata, isContractProxy } from '../abi-metadata'
4+
import { AbiMetaSymbol, getArc4Selector, getContractAbiMetadata, getContractMethodAbiMetadata, isContractProxy } from '../abi-metadata'
55
import { BytesMap } from '../collections/custom-key-map'
66
import { checkRoutingConditions } from '../context-helpers/context-util'
77
import { lazyContext } from '../context-helpers/internal-context'
@@ -235,8 +235,11 @@ export class ContractContext {
235235
if (prop === isContractProxy) {
236236
return true
237237
}
238-
const orig = Reflect.get(target, prop, receiver)
238+
if (prop === AbiMetaSymbol) {
239+
return isArc4 ? getContractAbiMetadata(target as Contract) : undefined
240+
}
239241
const abiMetadata = isArc4 ? getContractMethodAbiMetadata(target as Contract, prop as string) : undefined
242+
const orig = Reflect.get(target, prop, receiver)
240243
const isProgramMethod = prop === 'approvalProgram' || prop === 'clearStateProgram'
241244
const isAbiMethod = isArc4 && abiMetadata
242245
if (isAbiMethod || isProgramMethod) {
@@ -261,10 +264,6 @@ export class ContractContext {
261264

262265
onConstructed(application, instance, getContractOptions(t!))
263266

264-
if (isArc4) {
265-
copyAbiMetadatas(t! as Contract, instance as Contract)
266-
}
267-
268267
return instance
269268
},
270269
}

src/subcontexts/transaction-context.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { bytes, Contract, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { TransactionType } from '@algorandfoundation/algorand-typescript'
3-
import type { AbiMetadata } from '../abi-metadata'
4-
import { getContractMethodAbiMetadata } from '../abi-metadata'
3+
import { AbiMetaSymbol, type AbiMetadata } from '../abi-metadata'
54
import { TRANSACTION_GROUP_MAX_SIZE } from '../constants'
65
import { checkRoutingConditions } from '../context-helpers/context-util'
76
import { lazyContext } from '../context-helpers/internal-context'
@@ -202,7 +201,7 @@ export class TransactionContext {
202201
...args: TParams
203202
): DeferredAppCall<TParams, TReturn> {
204203
const appId = lazyContext.ledger.getApplicationForContract(contract)
205-
const abiMetadata = getContractMethodAbiMetadata(contract, methodName as string)
204+
const abiMetadata = (contract as DeliberateAny)[AbiMetaSymbol]?.[methodName]
206205
const txns = ContractContext.createMethodCallTxns(contract, abiMetadata, ...args)
207206
return new DeferredAppCall(appId.id, txns, method, abiMetadata, args)
208207
}

0 commit comments

Comments
 (0)