Skip to content

Commit 0480033

Browse files
authored
Merge pull request #15 from algorandfoundation/feat-arc4-types-in-tests
feat: implement arc4 method signature and method selector
2 parents c9061f6 + f01f372 commit 0480033

File tree

81 files changed

+12016
-2434
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+12016
-2434
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)

package-lock.json

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

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,16 @@
5555
"rollup": "^4.24.0",
5656
"semantic-release": "^24.1.2",
5757
"tsx": "4.19.1",
58-
"typescript": "5.6.2",
58+
"typescript": "^5.7.2",
5959
"vitest": "2.1.2"
6060
},
6161
"peerDependencies": {
6262
"tslib": "^2.6.2"
6363
},
6464
"dependencies": {
6565
"@algorandfoundation/algokit-utils": "^6.2.1",
66-
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.20",
67-
"@algorandfoundation/puya-ts": "^1.0.0-alpha.32",
66+
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.22",
67+
"@algorandfoundation/puya-ts": "^1.0.0-alpha.34",
6868
"algosdk": "^2.9.0",
6969
"elliptic": "^6.5.7",
7070
"js-sha256": "^0.11.0",
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
diff --git a/node_modules/typescript/lib/typescript.d.ts b/node_modules/typescript/lib/typescript.d.ts
2-
index 963c573..299a8a4 100644
2+
index 6780dd1..8700e72 100644
33
--- a/node_modules/typescript/lib/typescript.d.ts
44
+++ b/node_modules/typescript/lib/typescript.d.ts
5-
@@ -6116,6 +6116,7 @@ declare namespace ts {
5+
@@ -6159,6 +6159,7 @@ declare namespace ts {
66
getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo | undefined;
77
getIndexInfosOfType(type: Type): readonly IndexInfo[];
8-
getIndexInfosOfIndexSymbol: (indexSymbol: Symbol) => IndexInfo[];
8+
getIndexInfosOfIndexSymbol: (indexSymbol: Symbol, siblingSymbols?: Symbol[] | undefined) => IndexInfo[];
99
+ getTypeArgumentsForResolvedSignature(signature: Signature): readonly Type[] | undefined;
1010
getSignaturesOfType(type: Type, kind: SignatureKind): readonly Signature[];
1111
getIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined;
1212
getBaseTypes(type: InterfaceType): BaseType[];
1313
diff --git a/node_modules/typescript/lib/typescript.js b/node_modules/typescript/lib/typescript.js
14-
index 90f3266..9daa319 100644
14+
index 33387ea..a1f35b3 100644
1515
--- a/node_modules/typescript/lib/typescript.js
1616
+++ b/node_modules/typescript/lib/typescript.js
17-
@@ -50171,6 +50171,7 @@ function createTypeChecker(host) {
17+
@@ -50655,6 +50655,7 @@ function createTypeChecker(host) {
1818
getGlobalDiagnostics,
1919
getRecursionIdentity,
2020
getUnmatchedProperties,
2121
+ getTypeArgumentsForResolvedSignature,
2222
getTypeOfSymbolAtLocation: (symbol, locationIn) => {
2323
const location = getParseTreeNode(locationIn);
2424
return location ? getTypeOfSymbolAtLocation(symbol, location) : errorType;
25-
@@ -92895,6 +92896,9 @@ function createTypeChecker(host) {
26-
Debug.assert(specifier && nodeIsSynthesized(specifier) && specifier.text === "tslib", `Expected sourceFile.imports[0] to be the synthesized tslib import`);
27-
return specifier;
25+
@@ -71776,6 +71777,9 @@ function createTypeChecker(host) {
26+
}
27+
}
2828
}
2929
+ function getTypeArgumentsForResolvedSignature(signature) {
3030
+ return signature.mapper && instantiateTypes((signature.target ?? signature).typeParameters ?? [], signature.mapper);
3131
+ }
32-
}
33-
function isNotAccessor(declaration) {
34-
return !isAccessor(declaration);
32+
function getUnmatchedProperty(source, target, requireOptionalProperties, matchDiscriminantProperties) {
33+
return firstOrUndefinedIterator(getUnmatchedProperties(source, target, requireOptionalProperties, matchDiscriminantProperties));
34+
}

src/abi-metadata.ts

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
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
1114
allowActions?: OnCompleteActionStr[]
1215
}
1316
const AbiMetaSymbol = Symbol('AbiMetadata')
17+
export const isContractProxy = Symbol('isContractProxy')
1418
export const attachAbiMetadata = (contract: { new (): Contract }, methodName: string, metadata: AbiMetadata): void => {
1519
const metadatas: Record<string, AbiMetadata> = (AbiMetaSymbol in contract ? contract[AbiMetaSymbol] : {}) as Record<string, AbiMetadata>
1620
metadatas[methodName] = metadata
@@ -23,38 +27,89 @@ export const attachAbiMetadata = (contract: { new (): Contract }, methodName: st
2327
}
2428
}
2529

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+
2639
export const captureMethodConfig = <T extends Contract>(
2740
contract: T,
2841
methodName: string,
2942
config?: AbiMethodConfig<T> | BareMethodConfig,
3043
): void => {
31-
const metadata = ensureMetadata(contract, methodName)
44+
const metadata = getContractMethodAbiMetadata(contract, methodName)
3245
metadata.onCreate = config?.onCreate ?? 'disallow'
3346
metadata.allowActions = ([] as OnCompleteActionStr[]).concat(config?.allowActions ?? 'NoOp')
3447
}
3548

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-
4649
export const hasAbiMetadata = <T extends Contract>(contract: T): boolean => {
4750
const contractClass = contract.constructor as { new (): T }
4851
return (
4952
Object.getOwnPropertySymbols(contractClass).some((s) => s.toString() === AbiMetaSymbol.toString()) || AbiMetaSymbol in contractClass
5053
)
5154
}
52-
53-
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+
}
5459
const contractClass = contract.constructor as { new (): T }
5560
const s = Object.getOwnPropertySymbols(contractClass).find((s) => s.toString() === AbiMetaSymbol.toString())
5661
const metadatas: Record<string, AbiMetadata> = (
5762
s ? (contractClass as DeliberateAny)[s] : AbiMetaSymbol in contractClass ? contractClass[AbiMetaSymbol] : {}
5863
) 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)
5969
return metadatas[methodName]
6070
}
71+
72+
export const getArc4Signature = (metadata: AbiMetadata): string => {
73+
if (metadata.methodSignature === undefined) {
74+
const argTypes = metadata.argTypes.map((t) => JSON.parse(t) as TypeInfo).map(getArc4TypeName)
75+
const returnType = getArc4TypeName(JSON.parse(metadata.returnType) as TypeInfo)
76+
const method = new ABIMethod({ name: metadata.methodName, args: argTypes.map((t) => ({ type: t })), returns: { type: returnType } })
77+
metadata.methodSignature = method.getSignature()
78+
}
79+
return metadata.methodSignature
80+
}
81+
82+
const getArc4TypeName = (t: TypeInfo): string => {
83+
const map: Record<string, string | ((t: TypeInfo) => string)> = {
84+
void: 'void',
85+
account: 'account',
86+
application: 'application',
87+
asset: 'asset',
88+
boolean: 'bool',
89+
biguint: 'uint512',
90+
bytes: 'byte[]',
91+
string: 'string',
92+
uint64: 'uint64',
93+
OnCompleteAction: 'uint64',
94+
TransactionType: 'uint64',
95+
Transaction: 'txn',
96+
PaymentTxn: 'pay',
97+
KeyRegistrationTxn: 'keyreg',
98+
AssetConfigTxn: 'acfg',
99+
AssetTransferTxn: 'axfer',
100+
AssetFreezeTxn: 'afrz',
101+
ApplicationTxn: 'appl',
102+
'Tuple<.*>': (t) =>
103+
`(${Object.values(t.genericArgs as Record<string, TypeInfo>)
104+
.map(getArc4TypeName)
105+
.join(',')})`,
106+
}
107+
const entry = Object.entries(map).find(([k, _]) => new RegExp(`^${k}$`, 'i').test(t.name))?.[1]
108+
if (entry === undefined) {
109+
return getArc4TypeNameForARC4Encoded(t) ?? t.name
110+
}
111+
if (entry instanceof Function) {
112+
return entry(t)
113+
}
114+
return entry
115+
}
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/app-global.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Application, Bytes, bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { lazyContext } from '../context-helpers/internal-context'
3-
import { asBytes } from '../util'
3+
import { asBytes, toBytes } from '../util'
44
import { getApp } from './app-params'
55

66
export const AppGlobal: internal.opTypes.AppGlobalType = {
@@ -22,7 +22,7 @@ export const AppGlobal: internal.opTypes.AppGlobalType = {
2222
if (!exists) {
2323
return [Bytes(), false]
2424
}
25-
return [state!.value as bytes, exists]
25+
return [toBytes(state!.value), exists]
2626
},
2727
getExUint64(a: Application | internal.primitives.StubUint64Compat, b: internal.primitives.StubBytesCompat): readonly [uint64, boolean] {
2828
const app = getApp(a)

src/impl/app-local.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Account, Application, Bytes, bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { lazyContext } from '../context-helpers/internal-context'
3-
import { asBytes } from '../util'
3+
import { asBytes, toBytes } from '../util'
44
import { getAccount } from './acct-params'
55
import { getApp } from './app-params'
66

@@ -32,7 +32,7 @@ export const AppLocal: internal.opTypes.AppLocalType = {
3232
if (!exists) {
3333
return [Bytes(), false]
3434
}
35-
return [state!.value as bytes, exists]
35+
return [toBytes(state!.value), exists]
3636
},
3737
getExUint64: function (
3838
a: Account | internal.primitives.StubUint64Compat,

0 commit comments

Comments
 (0)