Skip to content

Commit

Permalink
Merge pull request #15 from algorandfoundation/feat-arc4-types-in-tests
Browse files Browse the repository at this point in the history
feat: implement arc4 method signature and method selector
  • Loading branch information
boblat authored Dec 18, 2024
2 parents c9061f6 + f01f372 commit 0480033
Show file tree
Hide file tree
Showing 81 changed files with 12,016 additions and 2,434 deletions.
4 changes: 2 additions & 2 deletions examples/calculator/contract.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { internal, op, Uint64 } from '@algorandfoundation/algorand-typescript'
import { internal, Uint64 } from '@algorandfoundation/algorand-typescript'
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
import { afterEach, describe, expect, it } from 'vitest'
import MyContract from './contract.algo'
Expand Down Expand Up @@ -31,7 +31,7 @@ describe('Calculator', () => {
.createScope([
ctx.any.txn.applicationCall({
appId: application,
appArgs: [op.itob(Uint64(1)), op.itob(Uint64(2)), op.itob(Uint64(3))],
appArgs: [Uint64(1), Uint64(2), Uint64(3)],
}),
])
.execute(contract.approvalProgram)
Expand Down
29 changes: 14 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@
"rollup": "^4.24.0",
"semantic-release": "^24.1.2",
"tsx": "4.19.1",
"typescript": "5.6.2",
"typescript": "^5.7.2",
"vitest": "2.1.2"
},
"peerDependencies": {
"tslib": "^2.6.2"
},
"dependencies": {
"@algorandfoundation/algokit-utils": "^6.2.1",
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.20",
"@algorandfoundation/puya-ts": "^1.0.0-alpha.32",
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.22",
"@algorandfoundation/puya-ts": "^1.0.0-alpha.34",
"algosdk": "^2.9.0",
"elliptic": "^6.5.7",
"js-sha256": "^0.11.0",
Expand Down
22 changes: 11 additions & 11 deletions patches/typescript+5.6.2.patch → patches/typescript+5.7.2.patch
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
diff --git a/node_modules/typescript/lib/typescript.d.ts b/node_modules/typescript/lib/typescript.d.ts
index 963c573..299a8a4 100644
index 6780dd1..8700e72 100644
--- a/node_modules/typescript/lib/typescript.d.ts
+++ b/node_modules/typescript/lib/typescript.d.ts
@@ -6116,6 +6116,7 @@ declare namespace ts {
@@ -6159,6 +6159,7 @@ declare namespace ts {
getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo | undefined;
getIndexInfosOfType(type: Type): readonly IndexInfo[];
getIndexInfosOfIndexSymbol: (indexSymbol: Symbol) => IndexInfo[];
getIndexInfosOfIndexSymbol: (indexSymbol: Symbol, siblingSymbols?: Symbol[] | undefined) => IndexInfo[];
+ getTypeArgumentsForResolvedSignature(signature: Signature): readonly Type[] | undefined;
getSignaturesOfType(type: Type, kind: SignatureKind): readonly Signature[];
getIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined;
getBaseTypes(type: InterfaceType): BaseType[];
diff --git a/node_modules/typescript/lib/typescript.js b/node_modules/typescript/lib/typescript.js
index 90f3266..9daa319 100644
index 33387ea..a1f35b3 100644
--- a/node_modules/typescript/lib/typescript.js
+++ b/node_modules/typescript/lib/typescript.js
@@ -50171,6 +50171,7 @@ function createTypeChecker(host) {
@@ -50655,6 +50655,7 @@ function createTypeChecker(host) {
getGlobalDiagnostics,
getRecursionIdentity,
getUnmatchedProperties,
+ getTypeArgumentsForResolvedSignature,
getTypeOfSymbolAtLocation: (symbol, locationIn) => {
const location = getParseTreeNode(locationIn);
return location ? getTypeOfSymbolAtLocation(symbol, location) : errorType;
@@ -92895,6 +92896,9 @@ function createTypeChecker(host) {
Debug.assert(specifier && nodeIsSynthesized(specifier) && specifier.text === "tslib", `Expected sourceFile.imports[0] to be the synthesized tslib import`);
return specifier;
@@ -71776,6 +71777,9 @@ function createTypeChecker(host) {
}
}
}
+ function getTypeArgumentsForResolvedSignature(signature) {
+ return signature.mapper && instantiateTypes((signature.target ?? signature).typeParameters ?? [], signature.mapper);
+ }
}
function isNotAccessor(declaration) {
return !isAccessor(declaration);
function getUnmatchedProperty(source, target, requireOptionalProperties, matchDiscriminantProperties) {
return firstOrUndefinedIterator(getUnmatchedProperties(source, target, requireOptionalProperties, matchDiscriminantProperties));
}
83 changes: 69 additions & 14 deletions src/abi-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { BaseContract, Contract } from '@algorandfoundation/algorand-typescript'
import { AbiMethodConfig, BareMethodConfig, CreateOptions, OnCompleteActionStr } from '@algorandfoundation/algorand-typescript/arc4'
import { ABIMethod } from 'algosdk'
import { TypeInfo } from './encoders'
import { getArc4TypeName as getArc4TypeNameForARC4Encoded } from './impl/encoded-types'
import { DeliberateAny } from './typescript-helpers'

export interface AbiMetadata {
methodName: string
methodSelector: string
methodSignature: string | undefined
argTypes: string[]
returnType: string
onCreate?: CreateOptions
allowActions?: OnCompleteActionStr[]
}
const AbiMetaSymbol = Symbol('AbiMetadata')
export const isContractProxy = Symbol('isContractProxy')
export const attachAbiMetadata = (contract: { new (): Contract }, methodName: string, metadata: AbiMetadata): void => {
const metadatas: Record<string, AbiMetadata> = (AbiMetaSymbol in contract ? contract[AbiMetaSymbol] : {}) as Record<string, AbiMetadata>
metadatas[methodName] = metadata
Expand All @@ -23,38 +27,89 @@ export const attachAbiMetadata = (contract: { new (): Contract }, methodName: st
}
}

export const copyAbiMetadatas = <T extends BaseContract>(sourceContract: T, targetContract: T): void => {
const metadatas = getContractAbiMetadata(sourceContract)
Object.defineProperty(targetContract, AbiMetaSymbol, {
value: metadatas,
writable: true,
enumerable: false,
})
}

export const captureMethodConfig = <T extends Contract>(
contract: T,
methodName: string,
config?: AbiMethodConfig<T> | BareMethodConfig,
): void => {
const metadata = ensureMetadata(contract, methodName)
const metadata = getContractMethodAbiMetadata(contract, methodName)
metadata.onCreate = config?.onCreate ?? 'disallow'
metadata.allowActions = ([] as OnCompleteActionStr[]).concat(config?.allowActions ?? 'NoOp')
}

const ensureMetadata = <T extends Contract>(contract: T, methodName: string): AbiMetadata => {
if (!hasAbiMetadata(contract)) {
const contractClass = contract.constructor as { new (): T }
Object.getOwnPropertyNames(Object.getPrototypeOf(contract)).forEach((name) => {
attachAbiMetadata(contractClass, name, { methodName: name, methodSelector: name, argTypes: [], returnType: '' })
})
}
return getAbiMetadata(contract, methodName)
}

export const hasAbiMetadata = <T extends Contract>(contract: T): boolean => {
const contractClass = contract.constructor as { new (): T }
return (
Object.getOwnPropertySymbols(contractClass).some((s) => s.toString() === AbiMetaSymbol.toString()) || AbiMetaSymbol in contractClass
)
}

export const getAbiMetadata = <T extends BaseContract>(contract: T, methodName: string): AbiMetadata => {
export const getContractAbiMetadata = <T extends BaseContract>(contract: T): Record<string, AbiMetadata> => {
if ((contract as DeliberateAny)[isContractProxy]) {
return (contract as DeliberateAny)[AbiMetaSymbol] as Record<string, AbiMetadata>
}
const contractClass = contract.constructor as { new (): T }
const s = Object.getOwnPropertySymbols(contractClass).find((s) => s.toString() === AbiMetaSymbol.toString())
const metadatas: Record<string, AbiMetadata> = (
s ? (contractClass as DeliberateAny)[s] : AbiMetaSymbol in contractClass ? contractClass[AbiMetaSymbol] : {}
) as Record<string, AbiMetadata>
return metadatas
}

export const getContractMethodAbiMetadata = <T extends BaseContract>(contract: T, methodName: string): AbiMetadata => {
const metadatas = getContractAbiMetadata(contract)
return metadatas[methodName]
}

export const getArc4Signature = (metadata: AbiMetadata): string => {
if (metadata.methodSignature === undefined) {
const argTypes = metadata.argTypes.map((t) => JSON.parse(t) as TypeInfo).map(getArc4TypeName)
const returnType = getArc4TypeName(JSON.parse(metadata.returnType) as TypeInfo)
const method = new ABIMethod({ name: metadata.methodName, args: argTypes.map((t) => ({ type: t })), returns: { type: returnType } })
metadata.methodSignature = method.getSignature()
}
return metadata.methodSignature
}

const getArc4TypeName = (t: TypeInfo): string => {
const map: Record<string, string | ((t: TypeInfo) => string)> = {
void: 'void',
account: 'account',
application: 'application',
asset: 'asset',
boolean: 'bool',
biguint: 'uint512',
bytes: 'byte[]',
string: 'string',
uint64: 'uint64',
OnCompleteAction: 'uint64',
TransactionType: 'uint64',
Transaction: 'txn',
PaymentTxn: 'pay',
KeyRegistrationTxn: 'keyreg',
AssetConfigTxn: 'acfg',
AssetTransferTxn: 'axfer',
AssetFreezeTxn: 'afrz',
ApplicationTxn: 'appl',
'Tuple<.*>': (t) =>
`(${Object.values(t.genericArgs as Record<string, TypeInfo>)
.map(getArc4TypeName)
.join(',')})`,
}
const entry = Object.entries(map).find(([k, _]) => new RegExp(`^${k}$`, 'i').test(t.name))?.[1]
if (entry === undefined) {
return getArc4TypeNameForARC4Encoded(t) ?? t.name
}
if (entry instanceof Function) {
return entry(t)
}
return entry
}
21 changes: 21 additions & 0 deletions src/context-helpers/context-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { internal, uint64 } from '@algorandfoundation/algorand-typescript'
import { AbiMetadata } from '../abi-metadata'
import { ApplicationTransaction } from '../impl/transactions'
import { lazyContext } from './internal-context'

export const checkRoutingConditions = (appId: uint64, metadata: AbiMetadata) => {
const appData = lazyContext.getApplicationData(appId)
const isCreating = appData.isCreating
if (isCreating && metadata.onCreate === 'disallow') {
throw new internal.errors.CodeError('method can not be called while creating')
}
if (!isCreating && metadata.onCreate === 'require') {
throw new internal.errors.CodeError('method can only be called while creating')
}
const txn = lazyContext.activeGroup.activeTransaction
if (txn instanceof ApplicationTransaction && metadata.allowActions && !metadata.allowActions.includes(txn.onCompletion)) {
throw new internal.errors.CodeError(
`method can only be called with one of the following on_completion values: ${metadata.allowActions.join(', ')}`,
)
}
}
8 changes: 5 additions & 3 deletions src/context-helpers/internal-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Account, internal } from '@algorandfoundation/algorand-typescript'
import { Account, BaseContract, internal } from '@algorandfoundation/algorand-typescript'
import { AccountData } from '../impl/account'
import { ApplicationData } from '../impl/application'
import { AssetData } from '../impl/asset'
Expand Down Expand Up @@ -60,8 +60,10 @@ class InternalContext {
return data
}

getApplicationData(id: internal.primitives.StubUint64Compat): ApplicationData {
const data = this.ledger.applicationDataMap.get(id)
getApplicationData(id: internal.primitives.StubUint64Compat | BaseContract): ApplicationData {
const uint64Id =
id instanceof BaseContract ? this.ledger.getApplicationForContract(id).id : internal.primitives.Uint64Cls.fromCompat(id)
const data = this.ledger.applicationDataMap.get(uint64Id)
if (!data) {
throw internal.errors.internalError('Unknown application, check correct testing context is active')
}
Expand Down
4 changes: 2 additions & 2 deletions src/impl/app-global.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Application, Bytes, bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'
import { asBytes } from '../util'
import { asBytes, toBytes } from '../util'
import { getApp } from './app-params'

export const AppGlobal: internal.opTypes.AppGlobalType = {
Expand All @@ -22,7 +22,7 @@ export const AppGlobal: internal.opTypes.AppGlobalType = {
if (!exists) {
return [Bytes(), false]
}
return [state!.value as bytes, exists]
return [toBytes(state!.value), exists]
},
getExUint64(a: Application | internal.primitives.StubUint64Compat, b: internal.primitives.StubBytesCompat): readonly [uint64, boolean] {
const app = getApp(a)
Expand Down
4 changes: 2 additions & 2 deletions src/impl/app-local.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Account, Application, Bytes, bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'
import { asBytes } from '../util'
import { asBytes, toBytes } from '../util'
import { getAccount } from './acct-params'
import { getApp } from './app-params'

Expand Down Expand Up @@ -32,7 +32,7 @@ export const AppLocal: internal.opTypes.AppLocalType = {
if (!exists) {
return [Bytes(), false]
}
return [state!.value as bytes, exists]
return [toBytes(state!.value), exists]
},
getExUint64: function (
a: Account | internal.primitives.StubUint64Compat,
Expand Down
Loading

0 comments on commit 0480033

Please sign in to comment.