Skip to content

Commit 58d87af

Browse files
committed
fix: ensure contract proxy has abimetadata;
fix: pass method name to deferAppCall; fix: add routing condition check; fix: capture state totals override from contract decorator
1 parent 81833af commit 58d87af

File tree

10 files changed

+254
-33
lines changed

10 files changed

+254
-33
lines changed

examples/calculator/contract.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { internal, op, Uint64 } from '@algorandfoundation/algorand-typescript'
1+
import { internal, Uint64 } from '@algorandfoundation/algorand-typescript'
22
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
33
import { afterEach, describe, expect, it } from 'vitest'
44
import MyContract from './contract.algo'
@@ -31,7 +31,7 @@ describe('Calculator', () => {
3131
.createScope([
3232
ctx.any.txn.applicationCall({
3333
appId: application,
34-
appArgs: [op.itob(Uint64(1)), op.itob(Uint64(2)), op.itob(Uint64(3))],
34+
appArgs: [Uint64(1), Uint64(2), Uint64(3)],
3535
}),
3636
])
3737
.execute(contract.approvalProgram)

src/abi-metadata.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface AbiMetadata {
1414
allowActions?: OnCompleteActionStr[]
1515
}
1616
const AbiMetaSymbol = Symbol('AbiMetadata')
17+
export const isContractProxy = Symbol('isContractProxy')
1718
export const attachAbiMetadata = (contract: { new (): Contract }, methodName: string, metadata: AbiMetadata): void => {
1819
const metadatas: Record<string, AbiMetadata> = (AbiMetaSymbol in contract ? contract[AbiMetaSymbol] : {}) as Record<string, AbiMetadata>
1920
metadatas[methodName] = metadata
@@ -26,12 +27,21 @@ export const attachAbiMetadata = (contract: { new (): Contract }, methodName: st
2627
}
2728
}
2829

30+
export const copyAbiMetadatas = <T extends BaseContract>(sourceContract: T, targetContract: T): void => {
31+
const metadatas = getContractAbiMetadata(sourceContract)
32+
Object.defineProperty(targetContract, AbiMetaSymbol, {
33+
value: metadatas,
34+
writable: true,
35+
enumerable: false,
36+
})
37+
}
38+
2939
export const captureMethodConfig = <T extends Contract>(
3040
contract: T,
3141
methodName: string,
3242
config?: AbiMethodConfig<T> | BareMethodConfig,
3343
): void => {
34-
const metadata = getAbiMetadata(contract, methodName)
44+
const metadata = getContractMethodAbiMetadata(contract, methodName)
3545
metadata.onCreate = config?.onCreate ?? 'disallow'
3646
metadata.allowActions = ([] as OnCompleteActionStr[]).concat(config?.allowActions ?? 'NoOp')
3747
}
@@ -42,13 +52,20 @@ export const hasAbiMetadata = <T extends Contract>(contract: T): boolean => {
4252
Object.getOwnPropertySymbols(contractClass).some((s) => s.toString() === AbiMetaSymbol.toString()) || AbiMetaSymbol in contractClass
4353
)
4454
}
45-
46-
export const getAbiMetadata = <T extends BaseContract>(contract: T, methodName: string): AbiMetadata => {
55+
export const getContractAbiMetadata = <T extends BaseContract>(contract: T): Record<string, AbiMetadata> => {
56+
if ((contract as DeliberateAny)[isContractProxy]) {
57+
return (contract as DeliberateAny)[AbiMetaSymbol] as Record<string, AbiMetadata>
58+
}
4759
const contractClass = contract.constructor as { new (): T }
4860
const s = Object.getOwnPropertySymbols(contractClass).find((s) => s.toString() === AbiMetaSymbol.toString())
4961
const metadatas: Record<string, AbiMetadata> = (
5062
s ? (contractClass as DeliberateAny)[s] : AbiMetaSymbol in contractClass ? contractClass[AbiMetaSymbol] : {}
5163
) as Record<string, AbiMetadata>
64+
return metadatas
65+
}
66+
67+
export const getContractMethodAbiMetadata = <T extends BaseContract>(contract: T, methodName: string): AbiMetadata => {
68+
const metadatas = getContractAbiMetadata(contract)
5269
return metadatas[methodName]
5370
}
5471

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { internal, uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { AbiMetadata } from '../abi-metadata'
3+
import { ApplicationTransaction } from '../impl/transactions'
4+
import { lazyContext } from './internal-context'
5+
6+
export const checkRoutingConditions = (appId: uint64, metadata: AbiMetadata) => {
7+
const appData = lazyContext.getApplicationData(appId)
8+
const isCreating = appData.isCreating
9+
if (isCreating && metadata.onCreate === 'disallow') {
10+
throw new internal.errors.CodeError('method can not be called while creating')
11+
}
12+
if (!isCreating && metadata.onCreate === 'require') {
13+
throw new internal.errors.CodeError('method can only be called while creating')
14+
}
15+
const txn = lazyContext.activeGroup.activeTransaction
16+
if (txn instanceof ApplicationTransaction && metadata.allowActions && !metadata.allowActions.includes(txn.onCompletion)) {
17+
throw new internal.errors.CodeError(
18+
`method can only be called with one of the following on_completion values: ${metadata.allowActions.join(', ')}`,
19+
)
20+
}
21+
}

src/context-helpers/internal-context.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Account, internal } from '@algorandfoundation/algorand-typescript'
1+
import { Account, BaseContract, internal } from '@algorandfoundation/algorand-typescript'
22
import { AccountData } from '../impl/account'
33
import { ApplicationData } from '../impl/application'
44
import { AssetData } from '../impl/asset'
@@ -60,8 +60,10 @@ class InternalContext {
6060
return data
6161
}
6262

63-
getApplicationData(id: internal.primitives.StubUint64Compat): ApplicationData {
64-
const data = this.ledger.applicationDataMap.get(id)
63+
getApplicationData(id: internal.primitives.StubUint64Compat | BaseContract): ApplicationData {
64+
const uint64Id =
65+
id instanceof BaseContract ? this.ledger.getApplicationForContract(id).id : internal.primitives.Uint64Cls.fromCompat(id)
66+
const data = this.ledger.applicationDataMap.get(uint64Id)
6567
if (!data) {
6668
throw internal.errors.internalError('Unknown application, check correct testing context is active')
6769
}

src/impl/inner-transactions.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { Account, Application, arc4, Asset, bytes, internal, itxn, TransactionType, uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { lazyContext } from '../context-helpers/internal-context'
3+
import { Mutable } from '../typescript-helpers'
4+
import { asBytes } from '../util'
5+
import { getApp } from './app-params'
6+
import { getAsset } from './asset-params'
7+
import { InnerTxn, InnerTxnFields } from './itxn'
28
import {
39
ApplicationTransaction,
410
AssetConfigTransaction,
@@ -7,12 +13,6 @@ import {
713
KeyRegistrationTransaction,
814
PaymentTransaction,
915
} from './transactions'
10-
import { asBytes, toBytes } from '../util'
11-
import { getAsset } from './asset-params'
12-
import { InnerTxn, InnerTxnFields } from './itxn'
13-
import { getApp } from './app-params'
14-
import { Mutable } from '../typescript-helpers'
15-
import { lazyContext } from '../context-helpers/internal-context'
1616

1717
const mapCommonFields = <T extends InnerTxnFields>(
1818
fields: T,
@@ -149,7 +149,7 @@ export class ApplicationInnerTxn extends ApplicationTransaction implements itxn.
149149
approvalProgramPages: Array.isArray(approvalProgram) ? approvalProgram : undefined,
150150
clearStateProgram: Array.isArray(clearStateProgram) ? undefined : (clearStateProgram as bytes),
151151
clearStateProgramPages: Array.isArray(clearStateProgram) ? clearStateProgram : undefined,
152-
appArgs: appArgs?.map((x) => toBytes(x)),
152+
appArgs: appArgs?.map((x) => x),
153153
accounts: accounts?.map((x) => x),
154154
assets: assets?.map((x) => x),
155155
apps: apps?.map((x) => x),

src/impl/transactions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export class AssetFreezeTransaction extends TransactionBase implements gtxn.Asse
226226

227227
export type ApplicationTransactionFields = TxnFields<gtxn.ApplicationTxn> &
228228
Partial<{
229-
appArgs: Array<bytes>
229+
appArgs: Array<unknown>
230230
accounts: Array<Account>
231231
assets: Array<Asset>
232232
apps: Array<Application>
@@ -241,7 +241,7 @@ export class ApplicationTransaction extends TransactionBase implements gtxn.Appl
241241
static create(fields: ApplicationTransactionFields) {
242242
return new ApplicationTransaction(fields)
243243
}
244-
#appArgs: Array<bytes>
244+
#appArgs: Array<unknown>
245245
#accounts: Array<Account>
246246
#assets: Array<Asset>
247247
#apps: Array<Application>
@@ -308,7 +308,7 @@ export class ApplicationTransaction extends TransactionBase implements gtxn.Appl
308308
return this.#appLogs.at(-1) ?? lazyContext.getApplicationData(this.appId.id).appLogs.at(-1) ?? Bytes()
309309
}
310310
appArgs(index: internal.primitives.StubUint64Compat): bytes {
311-
return this.#appArgs[asNumber(index)]
311+
return toBytes(this.#appArgs[asNumber(index)])
312312
}
313313
accounts(index: internal.primitives.StubUint64Compat): Account {
314314
return this.#accounts[asNumber(index)]

src/subcontexts/contract-context.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
1-
import { Account, Application, Asset, BaseContract, Bytes, bytes, Contract, LocalState } from '@algorandfoundation/algorand-typescript'
1+
import {
2+
Account,
3+
Application,
4+
Asset,
5+
BaseContract,
6+
Bytes,
7+
bytes,
8+
Contract,
9+
contract,
10+
LocalState,
11+
} from '@algorandfoundation/algorand-typescript'
212
import { ABIMethod } from 'algosdk'
3-
import { AbiMetadata, getAbiMetadata, getArc4Signature } from '../abi-metadata'
13+
import {
14+
AbiMetadata,
15+
copyAbiMetadatas,
16+
getArc4Signature,
17+
getContractAbiMetadata,
18+
getContractMethodAbiMetadata,
19+
isContractProxy,
20+
} from '../abi-metadata'
421
import { BytesMap } from '../collections/custom-key-map'
22+
import { checkRoutingConditions } from '../context-helpers/context-util'
523
import { lazyContext } from '../context-helpers/internal-context'
624
import type { TypeInfo } from '../encoders'
725
import { AccountCls } from '../impl/account'
@@ -21,6 +39,8 @@ import { getGenericTypeInfo } from '../runtime-helpers'
2139
import { DeliberateAny, IConstructor } from '../typescript-helpers'
2240
import { toBytes } from '../util'
2341

42+
type ContractOptionsParameter = Parameters<typeof contract>[0]
43+
2444
type StateTotals = Pick<Application, 'globalNumBytes' | 'globalNumUint' | 'localNumBytes' | 'localNumUint'>
2545

2646
interface States {
@@ -34,7 +54,7 @@ const isUint64GenericType = (typeInfo: TypeInfo | undefined) => {
3454
return typeInfo.genericArgs.some((t) => t.name.toLocaleLowerCase() === 'uint64')
3555
}
3656

37-
const extractStates = (contract: BaseContract): States => {
57+
const extractStates = (contract: BaseContract, contractOptions: ContractOptionsParameter | undefined): States => {
3858
const stateTotals = { globalNumBytes: 0, globalNumUint: 0, localNumBytes: 0, localNumUint: 0 }
3959
const states = {
4060
globalStates: new BytesMap<GlobalStateCls<unknown>>(),
@@ -67,6 +87,12 @@ const extractStates = (contract: BaseContract): States => {
6787
stateTotals.localNumBytes += isLocalState && !isUint64State ? 1 : 0
6888
}
6989
})
90+
91+
stateTotals.globalNumUint = contractOptions?.stateTotals?.globalUints ?? stateTotals.globalNumUint
92+
stateTotals.globalNumBytes = contractOptions?.stateTotals?.globalBytes ?? stateTotals.globalNumBytes
93+
stateTotals.localNumUint = contractOptions?.stateTotals?.localUints ?? stateTotals.localNumUint
94+
stateTotals.localNumBytes = contractOptions?.stateTotals?.localBytes ?? stateTotals.localNumBytes
95+
7096
return states
7197
}
7298

@@ -142,8 +168,8 @@ export class ContractContext {
142168
}
143169

144170
private getContractProxyHandler<T extends BaseContract>(isArc4: boolean): ProxyHandler<IConstructor<T>> {
145-
const onConstructed = (application: Application, instance: T) => {
146-
const states = extractStates(instance)
171+
const onConstructed = (application: Application, instance: T, conrtactOptions: ContractOptionsParameter | undefined) => {
172+
const states = extractStates(instance, conrtactOptions)
147173

148174
const applicationData = lazyContext.ledger.applicationDataMap.getOrFail(application.id)
149175
applicationData.application = {
@@ -159,23 +185,33 @@ export class ContractContext {
159185
let t: T | undefined = undefined
160186
const application = lazyContext.any.application()
161187
const txn = lazyContext.any.txn.applicationCall({ appId: application })
188+
const appData = lazyContext.ledger.applicationDataMap.getOrFail(application.id)
189+
appData.isCreating = true
162190
lazyContext.txn.ensureScope([txn]).execute(() => {
163191
t = new target(...args)
164192
})
193+
appData.isCreating = hasCreateMethods(t!)
165194
const instance = new Proxy(t!, {
166195
get(target, prop, receiver) {
196+
if (prop === isContractProxy) {
197+
return true
198+
}
167199
const orig = Reflect.get(target, prop, receiver)
168-
const abiMetadata = getAbiMetadata(target, prop as string)
200+
const abiMetadata = getContractMethodAbiMetadata(target, prop as string)
169201
const isProgramMethod = prop === 'approvalProgram' || prop === 'clearStateProgram'
170202
const isAbiMethod = isArc4 && abiMetadata
171203
if (isAbiMethod || isProgramMethod) {
172204
return (...args: DeliberateAny[]): DeliberateAny => {
173205
const txns = ContractContext.createMethodCallTxns(receiver, abiMetadata, ...args)
174206
return lazyContext.txn.ensureScope(txns).execute(() => {
207+
if (isAbiMethod) {
208+
checkRoutingConditions(application.id, abiMetadata)
209+
}
175210
const returnValue = (orig as DeliberateAny).apply(target, args)
176211
if (!isProgramMethod && isAbiMethod && returnValue !== undefined) {
177212
;(txns.at(-1) as ApplicationTransaction).logArc4ReturnValue(returnValue)
178213
}
214+
appData.isCreating = false
179215
return returnValue
180216
})
181217
}
@@ -184,10 +220,18 @@ export class ContractContext {
184220
},
185221
})
186222

187-
onConstructed(application, instance)
223+
onConstructed(application, instance, (t!.constructor as DeliberateAny).contractOptions)
224+
225+
copyAbiMetadatas(t!, instance)
188226

189227
return instance
190228
},
191229
}
192230
}
193231
}
232+
233+
// TODO: add test_contract.py
234+
const hasCreateMethods = (contract: BaseContract) => {
235+
const metadatas = getContractAbiMetadata(contract)
236+
return Object.values(metadatas).some((metadata) => (metadata.onCreate ?? 'disallow') !== 'disallow')
237+
}

src/subcontexts/transaction-context.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { bytes, Contract, internal, TransactionType, uint64 } from '@algorandfoundation/algorand-typescript'
22
import algosdk from 'algosdk'
3-
import { getAbiMetadata } from '../abi-metadata'
3+
import { AbiMetadata, getContractMethodAbiMetadata } from '../abi-metadata'
44
import { lazyContext } from '../context-helpers/internal-context'
55
import { DecodedLogs, decodeLogs, LogDecoding } from '../decode-logs'
66
import { testInvariant } from '../errors'
@@ -24,6 +24,7 @@ import {
2424
PaymentTransaction,
2525
Transaction,
2626
} from '../impl/transactions'
27+
import { FunctionKeys } from '../typescript-helpers'
2728
import { asBigInt, asNumber, asUint64 } from '../util'
2829
import { ContractContext } from './contract-context'
2930

@@ -48,16 +49,34 @@ interface ExecutionScope {
4849
execute: <TReturn>(body: () => TReturn) => TReturn
4950
}
5051

52+
export const checkRoutingConditions = (appId: uint64, metadata: AbiMetadata) => {
53+
const appData = lazyContext.getApplicationData(appId)
54+
const isCreating = appData.isCreating
55+
if (isCreating && metadata.onCreate === 'disallow') {
56+
throw new internal.errors.CodeError('method can not be called while creating')
57+
}
58+
if (!isCreating && metadata.onCreate === 'require') {
59+
throw new internal.errors.CodeError('method can only be called while creating')
60+
}
61+
const txn = lazyContext.activeGroup.activeTransaction
62+
if (txn instanceof ApplicationTransaction && metadata.allowActions && !metadata.allowActions.includes(txn.onCompletion)) {
63+
throw new internal.errors.CodeError(
64+
`method can only be called with one of the following on_completion values: ${metadata.allowActions.join(', ')}`,
65+
)
66+
}
67+
}
68+
5169
export class DeferredAppCall<TParams extends unknown[], TReturn> {
5270
constructor(
5371
private readonly appId: uint64,
5472
readonly txns: Transaction[],
5573
private readonly method: (...args: TParams) => TReturn,
74+
private readonly abiMetadata: AbiMetadata,
5675
private readonly args: TParams,
5776
) {}
5877

5978
submit(): TReturn {
60-
// TODO: check_routing_conditions
79+
checkRoutingConditions(this.appId, this.abiMetadata)
6180
return this.method(...this.args)
6281
}
6382
}
@@ -125,15 +144,16 @@ export class TransactionContext {
125144
activeTransaction.appendLog(value)
126145
}
127146

128-
deferAppCall<TParams extends unknown[], TReturn>(
129-
contract: Contract,
147+
deferAppCall<TContract extends Contract, TParams extends unknown[], TReturn>(
148+
contract: TContract,
130149
method: (...args: TParams) => TReturn,
150+
methodName: FunctionKeys<TContract>,
131151
...args: TParams
132152
): DeferredAppCall<TParams, TReturn> {
133153
const appId = lazyContext.ledger.getApplicationForContract(contract)
134-
const abiMetadata = getAbiMetadata(contract, method.name)
154+
const abiMetadata = getContractMethodAbiMetadata(contract, methodName as string)
135155
const txns = ContractContext.createMethodCallTxns(contract, abiMetadata, ...args)
136-
return new DeferredAppCall(appId.id, txns, method, args)
156+
return new DeferredAppCall(appId.id, txns, method, abiMetadata, args)
137157
}
138158

139159
exportLogs<const T extends [...LogDecoding[]]>(appId: uint64, ...decoding: T): DecodedLogs<T> {

0 commit comments

Comments
 (0)