Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
"vitest": "3.2.4"
},
"dependencies": {
"@algorandfoundation/algorand-typescript": "1.0.1",
"@algorandfoundation/puya-ts": "1.0.1",
"@algorandfoundation/algorand-typescript": "1.1.0-beta.1",
"@algorandfoundation/puya-ts": "1.1.0-beta.1",
"elliptic": "^6.6.1",
"js-sha256": "^0.11.0",
"js-sha3": "^0.9.3",
Expand Down
21 changes: 20 additions & 1 deletion src/abi-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { arc4, OnCompleteActionStr } from '@algorandfoundation/algorand-typescript'
import js_sha512 from 'js-sha512'
import { ConventionalRouting } from './constants'
import { InternalError } from './errors'
import { Arc4MethodConfigSymbol, Contract } from './impl/contract'
import type { TypeInfo } from './impl/encoded-types'
import { getArc4TypeName } from './impl/encoded-types'
import type { DeliberateAny } from './typescript-helpers'
import type { DeliberateAny, InstanceMethod } from './typescript-helpers'

/** @internal */
export interface AbiMetadata {
Expand Down Expand Up @@ -109,6 +110,24 @@ export const getArc4Selector = (metadata: AbiMetadata): Uint8Array => {
return new Uint8Array(hash.slice(0, 4))
}

/** @internal */
export const getContractMethod = (contractFullName: string, methodName: string) => {
const contract = getContractByName(contractFullName)

if (contract === undefined || typeof contract !== 'function') {
throw new InternalError(`Unknown contract: ${contractFullName}`)
}

if (!Object.hasOwn(contract.prototype, methodName)) {
throw new InternalError(`Unknown method: ${methodName} in contract: ${contractFullName}`)
}

return {
method: contract.prototype[methodName] as InstanceMethod<Contract, DeliberateAny[]>,
contract,
}
}

/**
* Get routing properties inferred by conventional naming
* @param methodName The name of the method
Expand Down
2 changes: 1 addition & 1 deletion src/application-spy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class ApplicationSpy<TContract extends Contract = Contract> {
ocas = metadata.allowActions?.map((action) => OnCompleteAction[action]) ?? [OnCompleteAction.NoOp]
}

const selector = methodSelector(fn, spy.contract)
const selector = methodSelector({ method: fn, contract: spy.contract })
spy.onAbiCall(selector, ocas, callback)
}
},
Expand Down
13 changes: 5 additions & 8 deletions src/impl/c2c.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import type {
ContractProxy,
TypedApplicationCallFields,
} from '@algorandfoundation/algorand-typescript/arc4'
import { getContractByName, getContractMethodAbiMetadata } from '../abi-metadata'
import { getContractMethod, getContractMethodAbiMetadata } from '../abi-metadata'
import { lazyContext } from '../context-helpers/internal-context'
import { InternalError } from '../errors'
import type { ConstructorFor, DeliberateAny, InstanceMethod } from '../typescript-helpers'
import type { ApplicationCallInnerTxn } from './inner-transactions'
import { ApplicationCallInnerTxnContext } from './inner-transactions'
Expand Down Expand Up @@ -34,7 +33,7 @@ export function compileArc4<TContract extends Contract>(
call: new Proxy({} as unknown as TContract, {
get: (_target, prop) => {
return (methodArgs: TypedApplicationCallFields<DeliberateAny[]>) => {
const selector = methodSelector(prop as string, contract)
const selector = methodSelector({ method: prop as string, contract })
const abiMetadata = getContractMethodAbiMetadata(contract, prop as string)
const onCompleteActions = abiMetadata?.allowActions?.map((action) => OnCompleteAction[action])
const itxnContext = ApplicationCallInnerTxnContext.createFromTypedApplicationCallFields(
Expand Down Expand Up @@ -95,7 +94,7 @@ export function getApplicationCallInnerTxnContext<TArgs extends DeliberateAny[],
contract?: Contract | { new (): Contract },
) {
const abiMetadata = contract ? getContractMethodAbiMetadata(contract, method.name) : undefined
const selector = methodSelector(method, contract)
const selector = methodSelector({ method, contract })
return ApplicationCallInnerTxnContext.createFromTypedApplicationCallFields<TReturn>(
{
...methodArgs,
Expand All @@ -111,11 +110,9 @@ export function abiCall<TArgs extends DeliberateAny[], TReturn>(
method: string,
methodArgs: TypedApplicationCallFields<TArgs>,
): { itxn: ApplicationCallInnerTxn; returnValue: TReturn | undefined } {
const contract = getContractByName(contractFullName)
if (contract === undefined || typeof contract !== 'function') throw new InternalError(`Unknown contract: ${contractFullName}`)
if (!Object.hasOwn(contract.prototype, method)) throw new InternalError(`Unknown method: ${method} in contract: ${contractFullName}`)
const { method: methodInstance, contract: contractInstance } = getContractMethod(contractFullName, method)

const itxnContext = getApplicationCallInnerTxnContext<TArgs, TReturn>(contract.prototype[method], methodArgs, contract)
const itxnContext = getApplicationCallInnerTxnContext<TArgs, TReturn>(methodInstance, methodArgs, contractInstance)

invokeAbiCall(itxnContext)

Expand Down
65 changes: 43 additions & 22 deletions src/impl/itxn-compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import type {
KeyRegistrationComposeFields,
PaymentComposeFields,
} from '@algorandfoundation/algorand-typescript'
import type { TypedApplicationCallFields } from '@algorandfoundation/algorand-typescript/arc4'
import type { AbiCallOptions, TypedApplicationCallFields } from '@algorandfoundation/algorand-typescript/arc4'
import { getContractMethod } from '../abi-metadata'
import { lazyContext } from '../context-helpers/internal-context'
import type { DeliberateAny, InstanceMethod } from '../typescript-helpers'
import { getApplicationCallInnerTxnContext } from './c2c'
Expand All @@ -29,16 +30,9 @@ class ItxnCompose {
fields: TypedApplicationCallFields<TArgs>,
contract?: Contract | { new (): Contract },
): void
begin<TArgs extends DeliberateAny[]>(...args: unknown[]): void {
lazyContext.txn.activeGroup.constructingItxnGroup.push(
args.length === 1
? (args[0] as AnyTransactionComposeFields)
: getApplicationCallInnerTxnContext(
args[0] as InstanceMethod<Contract, TArgs>,
args[1] as TypedApplicationCallFields<TArgs>,
args[2] as Contract | { new (): Contract },
),
)
begin<TMethod>(options: AbiCallOptions<TMethod>, contract: string, method: string): void
begin(...args: unknown[]): void {
this.addInnerTransaction(...args)
}

next(fields: PaymentComposeFields): void
Expand All @@ -49,22 +43,49 @@ class ItxnCompose {
next(fields: ApplicationCallComposeFields): void
next(fields: AnyTransactionComposeFields): void
next(fields: ComposeItxnParams): void
next<TArgs extends DeliberateAny[]>(_method: InstanceMethod<Contract, TArgs>, _fields: TypedApplicationCallFields<TArgs>): void
next<TArgs extends DeliberateAny[]>(...args: unknown[]): void {
lazyContext.txn.activeGroup.constructingItxnGroup.push(
args.length === 1
? (args[0] as AnyTransactionComposeFields)
: getApplicationCallInnerTxnContext(
args[0] as InstanceMethod<Contract, TArgs>,
args[1] as TypedApplicationCallFields<TArgs>,
args[2] as Contract | { new (): Contract },
),
)
next<TArgs extends DeliberateAny[]>(
_method: InstanceMethod<Contract, TArgs>,
_fields: TypedApplicationCallFields<TArgs>,
contract?: Contract | { new (): Contract },
): void
next<TMethod>(options: AbiCallOptions<TMethod>, contract: string, method: string): void
next(...args: unknown[]): void {
this.addInnerTransaction(...args)
}

submit(): void {
lazyContext.txn.activeGroup.submitInnerTransactionGroup()
}

private addInnerTransaction<TArgs extends DeliberateAny[]>(...args: unknown[]): void {
let innerTxnFields

// Single argument: direct transaction fields
if (args.length === 1) {
innerTxnFields = args[0] as AnyTransactionComposeFields
}
// Three arguments with object fields (deprecated signature):
// e.g. `itxnCompose.begin(Hello.prototype.greet, { appId, args: ['ho'] })`
else if (args.length === 3 && typeof args[1] === 'object') {
innerTxnFields = getApplicationCallInnerTxnContext(
args[0] as InstanceMethod<Contract, TArgs>,
args[1] as TypedApplicationCallFields<TArgs>,
args[2] as Contract | { new (): Contract },
)
}
// Three arguments with string contract name:
// e.g. `itxnCompose.next({ method: Hello.prototype.greet, appId, args: ['ho'] })`
// or `itxnCompose.next<typeof Hello.prototype.greet>({ appId, args: ['ho'] })`
else {
const contractFullName = args[1] as string
const methodName = args[2] as string
const { method, contract } = getContractMethod(contractFullName, methodName)

innerTxnFields = getApplicationCallInnerTxnContext(method, args[0] as TypedApplicationCallFields<TArgs>, contract)
}

lazyContext.txn.activeGroup.constructingItxnGroup.push(innerTxnFields)
}
}

/** @internal */
Expand Down
64 changes: 49 additions & 15 deletions src/impl/method-selector.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,57 @@
import type { arc4, bytes } from '@algorandfoundation/algorand-typescript'
import type { bytes } from '@algorandfoundation/algorand-typescript'
import { encodingUtil } from '@algorandfoundation/puya-ts'
import { getArc4Selector, getContractMethodAbiMetadata } from '../abi-metadata'
import type { Overloads } from '../typescript-helpers'
import { getArc4Selector, getContractMethod, getContractMethodAbiMetadata } from '../abi-metadata'
import { CodeError } from '../errors'
import type { InstanceMethod } from '../typescript-helpers'
import type { Contract } from './contract'
import { sha512_256 } from './crypto'
import { Bytes } from './primitives'

/** @internal */
export const methodSelector = <TContract extends Contract>(
methodSignature: Parameters<Overloads<typeof arc4.methodSelector>>[0],
contract?: TContract | { new (): TContract },
): bytes => {
if (typeof methodSignature === 'string' && contract === undefined) {
return sha512_256(Bytes(encodingUtil.utf8ToUint8Array(methodSignature))).slice(0, 4)
} else {
const abiMetadata = getContractMethodAbiMetadata(
contract!,
typeof methodSignature === 'string' ? methodSignature : methodSignature.name,
)
/**
* Computes the method selector for an ARC-4 contract method.
*
* Supports three invocation patterns:
* 1. `methodSelector('sink(string,uint8[])void')`:
* Direct method signature string (no contract): Returns SHA-512/256 hash of signature
* 2. `methodSelector<typeof SignaturesContract.prototype.sink>()`:
* Contract name as string + method name as string: Looks up registered contract and returns ARC-4 selector
* 3. `methodSelector(SignaturesContract.prototype.sink)`:
* Contract class/instance + method instance/name: Returns ARC-4 selector from ABI metadata
*
* @internal
*/
export const methodSelector = <TContract extends Contract>({
method,
contract,
}: {
method?: string | InstanceMethod<Contract>
contract?: string | TContract | { new (): TContract }
}): bytes => {
const isDirectSignature = typeof method === 'string' && contract === undefined
const isContractNameLookup = typeof contract === 'string' && typeof method === 'string' && contract && method
const isContractMethodLookup = typeof contract !== 'string' && contract && method

// Pattern 1: Direct method signature string (e.g., "add(uint64,uint64)uint64")
if (isDirectSignature) {
const signatureBytes = Bytes(encodingUtil.utf8ToUint8Array(method as string))
return sha512_256(signatureBytes).slice(0, 4)
}

// Pattern 2: Contract name as string with method name
if (isContractNameLookup) {
const { contract: registeredContract } = getContractMethod(contract, method)

const abiMetadata = getContractMethodAbiMetadata(registeredContract, method)
return Bytes(getArc4Selector(abiMetadata))
}

// Pattern 3: Contract class/instance with method signature or name
if (isContractMethodLookup) {
const methodName = typeof method === 'string' ? method : method.name

const abiMetadata = getContractMethodAbiMetadata(contract, methodName)
return Bytes(getArc4Selector(abiMetadata))
}

throw new CodeError('Invalid arguments to methodSelector')
}
34 changes: 29 additions & 5 deletions src/test-transformer/node-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,31 @@ export const nodeFactory = {
return factory.updateCallExpression(node, node.expression, node.typeArguments, [...typeInfoArgs, ...(node.arguments ?? [])])
},

callMethodSelectorFunction(node: ts.CallExpression) {
if (
callMethodSelectorFunction(node: ts.CallExpression, typeParams: ptypes.PType[]) {
if (typeParams.length === 1 && typeParams[0] instanceof ptypes.FunctionPType && typeParams[0].declaredIn) {
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
factory.createObjectLiteralExpression([
factory.createPropertyAssignment('method', factory.createStringLiteral(typeParams[0].name)),
factory.createPropertyAssignment('contract', factory.createStringLiteral(typeParams[0].declaredIn.fullName)),
]),
])
} else if (
node.arguments.length === 1 &&
ts.isPropertyAccessExpression(node.arguments[0]) &&
ts.isPropertyAccessExpression(node.arguments[0].expression)
) {
const contractIdenifier = node.arguments[0].expression.expression
return factory.updateCallExpression(node, node.expression, node.typeArguments, [...node.arguments, contractIdenifier])
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
factory.createObjectLiteralExpression([
factory.createPropertyAssignment('method', node.arguments[0]),
factory.createPropertyAssignment('contract', contractIdenifier),
]),
])
} else {
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
factory.createObjectLiteralExpression([factory.createPropertyAssignment('method', node.arguments[0])]),
])
}
return node
},

callAbiCallFunction(node: ts.CallExpression, typeParams: ptypes.PType[]) {
Expand All @@ -144,14 +159,23 @@ export const nodeFactory = {
return node
},

callItxnComposeFunction(node: ts.CallExpression) {
callItxnComposeFunction(node: ts.CallExpression, typeParams: ptypes.PType[]) {
if (
node.arguments.length === 2 &&
ts.isPropertyAccessExpression(node.arguments[0]) &&
ts.isPropertyAccessExpression(node.arguments[0].expression)
) {
const contractIdenifier = node.arguments[0].expression.expression
return factory.updateCallExpression(node, node.expression, node.typeArguments, [...node.arguments, contractIdenifier])
} else if (
node.arguments.length === 1 &&
typeParams.length === 1 &&
typeParams[0] instanceof ptypes.FunctionPType &&
typeParams[0].declaredIn
) {
const contractIdentifier = factory.createStringLiteral(typeParams[0].declaredIn.fullName)
const methodName = factory.createStringLiteral(typeParams[0].name)
return factory.updateCallExpression(node, node.expression, node.typeArguments, [...node.arguments, contractIdentifier, methodName])
}
return node
},
Expand Down
6 changes: 4 additions & 2 deletions src/test-transformer/visitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,14 @@ class ExpressionVisitor {

if (stubbedFunctionName) {
if (isCallingMethodSelector(stubbedFunctionName)) {
updatedNode = nodeFactory.callMethodSelectorFunction(updatedNode)
const typeParams = this.helper.resolveTypeParameters(updatedNode)
updatedNode = nodeFactory.callMethodSelectorFunction(updatedNode, typeParams)
} else if (isCallingAbiCall(stubbedFunctionName)) {
const typeParams = this.helper.resolveTypeParameters(updatedNode)
updatedNode = nodeFactory.callAbiCallFunction(updatedNode, typeParams)
} else if (isCallingItxnCompose(stubbedFunctionName)) {
updatedNode = nodeFactory.callItxnComposeFunction(updatedNode)
const typeParams = this.helper.resolveTypeParameters(updatedNode)
updatedNode = nodeFactory.callItxnComposeFunction(updatedNode, typeParams)
} else {
updatedNode = nodeFactory.callStubbedFunction(updatedNode, infoArg)
}
Expand Down
Loading