Skip to content

Commit 6628bf0

Browse files
committed
feat: add new overload for methodSelector to support type only import
1 parent 10ec44d commit 6628bf0

File tree

6 files changed

+117
-23
lines changed

6 files changed

+117
-23
lines changed

src/application-spy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class ApplicationSpy<TContract extends Contract = Contract> {
112112
ocas = metadata.allowActions?.map((action) => OnCompleteAction[action]) ?? [OnCompleteAction.NoOp]
113113
}
114114

115-
const selector = methodSelector(fn, spy.contract)
115+
const selector = methodSelector({ method: fn, contract: spy.contract })
116116
spy.onAbiCall(selector, ocas, callback)
117117
}
118118
},

src/impl/c2c.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function compileArc4<TContract extends Contract>(
3434
call: new Proxy({} as unknown as TContract, {
3535
get: (_target, prop) => {
3636
return (methodArgs: TypedApplicationCallFields<DeliberateAny[]>) => {
37-
const selector = methodSelector(prop as string, contract)
37+
const selector = methodSelector({ method: prop as string, contract })
3838
const abiMetadata = getContractMethodAbiMetadata(contract, prop as string)
3939
const onCompleteActions = abiMetadata?.allowActions?.map((action) => OnCompleteAction[action])
4040
const itxnContext = ApplicationCallInnerTxnContext.createFromTypedApplicationCallFields(
@@ -95,7 +95,7 @@ export function getApplicationCallInnerTxnContext<TArgs extends DeliberateAny[],
9595
contract?: Contract | { new (): Contract },
9696
) {
9797
const abiMetadata = contract ? getContractMethodAbiMetadata(contract, method.name) : undefined
98-
const selector = methodSelector(method, contract)
98+
const selector = methodSelector({ method, contract })
9999
return ApplicationCallInnerTxnContext.createFromTypedApplicationCallFields<TReturn>(
100100
{
101101
...methodArgs,

src/impl/method-selector.ts

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,65 @@
1-
import type { arc4, bytes } from '@algorandfoundation/algorand-typescript'
1+
import type { bytes } from '@algorandfoundation/algorand-typescript'
22
import { encodingUtil } from '@algorandfoundation/puya-ts'
3-
import { getArc4Selector, getContractMethodAbiMetadata } from '../abi-metadata'
4-
import type { Overloads } from '../typescript-helpers'
3+
import { getArc4Selector, getContractByName, getContractMethodAbiMetadata } from '../abi-metadata'
4+
import { CodeError, InternalError } from '../errors'
5+
import type { InstanceMethod } from '../typescript-helpers'
56
import type { Contract } from './contract'
67
import { sha512_256 } from './crypto'
78
import { Bytes } from './primitives'
89

9-
/** @internal */
10-
export const methodSelector = <TContract extends Contract>(
11-
methodSignature: Parameters<Overloads<typeof arc4.methodSelector>>[0],
12-
contract?: TContract | { new (): TContract },
13-
): bytes => {
14-
if (typeof methodSignature === 'string' && contract === undefined) {
15-
return sha512_256(Bytes(encodingUtil.utf8ToUint8Array(methodSignature))).slice(0, 4)
16-
} else {
17-
const abiMetadata = getContractMethodAbiMetadata(
18-
contract!,
19-
typeof methodSignature === 'string' ? methodSignature : methodSignature.name,
20-
)
10+
/**
11+
* Computes the method selector for an ARC-4 contract method.
12+
*
13+
* Supports three invocation patterns:
14+
* 1. `methodSelector('sink(string,uint8[])void')`:
15+
* Direct method signature string (no contract): Returns SHA-512/256 hash of signature
16+
* 2. `methodSelector<typeof SignaturesContract.prototype.sink>()`:
17+
* Contract name as string + method name as string: Looks up registered contract and returns ARC-4 selector
18+
* 3. `methodSelector(SignaturesContract.prototype.sink)`:
19+
* Contract class/instance + method instance/name: Returns ARC-4 selector from ABI metadata
20+
*
21+
* @internal
22+
*/
23+
export const methodSelector = <TContract extends Contract>({
24+
method,
25+
contract,
26+
}: {
27+
method?: string | InstanceMethod<Contract>
28+
contract?: string | TContract | { new (): TContract }
29+
}): bytes => {
30+
const isDirectSignature = typeof method === 'string' && contract === undefined
31+
const isContractNameLookup = typeof contract === 'string' && typeof method === 'string' && contract && method
32+
const isContractMethodLookup = typeof contract !== 'string' && contract && method
33+
34+
// Pattern 1: Direct method signature string (e.g., "add(uint64,uint64)uint64")
35+
if (isDirectSignature) {
36+
const signatureBytes = Bytes(encodingUtil.utf8ToUint8Array(method as string))
37+
return sha512_256(signatureBytes).slice(0, 4)
38+
}
39+
40+
// Pattern 2: Contract name as string with method name
41+
if (isContractNameLookup) {
42+
const registeredContract = getContractByName(contract)
43+
44+
if (registeredContract === undefined || typeof registeredContract !== 'function') {
45+
throw new InternalError(`Unknown contract: ${contract}`)
46+
}
47+
48+
if (!Object.hasOwn(registeredContract.prototype, method)) {
49+
throw new InternalError(`Unknown method: ${method} in contract: ${contract}`)
50+
}
51+
52+
const abiMetadata = getContractMethodAbiMetadata(registeredContract, method)
2153
return Bytes(getArc4Selector(abiMetadata))
2254
}
55+
56+
// Pattern 3: Contract class/instance with method signature or name
57+
if (isContractMethodLookup) {
58+
const methodName = typeof method === 'string' ? method : method.name
59+
60+
const abiMetadata = getContractMethodAbiMetadata(contract, methodName)
61+
return Bytes(getArc4Selector(abiMetadata))
62+
}
63+
64+
throw new CodeError('Invalid arguments to methodSelector')
2365
}

src/test-transformer/node-factory.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,31 @@ export const nodeFactory = {
121121
return factory.updateCallExpression(node, node.expression, node.typeArguments, [...typeInfoArgs, ...(node.arguments ?? [])])
122122
},
123123

124-
callMethodSelectorFunction(node: ts.CallExpression) {
125-
if (
124+
callMethodSelectorFunction(node: ts.CallExpression, typeParams: ptypes.PType[]) {
125+
if (typeParams.length === 1 && typeParams[0] instanceof ptypes.FunctionPType && typeParams[0].declaredIn) {
126+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
127+
factory.createObjectLiteralExpression([
128+
factory.createPropertyAssignment('method', factory.createStringLiteral(typeParams[0].name)),
129+
factory.createPropertyAssignment('contract', factory.createStringLiteral(typeParams[0].declaredIn.fullName)),
130+
]),
131+
])
132+
} else if (
126133
node.arguments.length === 1 &&
127134
ts.isPropertyAccessExpression(node.arguments[0]) &&
128135
ts.isPropertyAccessExpression(node.arguments[0].expression)
129136
) {
130137
const contractIdenifier = node.arguments[0].expression.expression
131-
return factory.updateCallExpression(node, node.expression, node.typeArguments, [...node.arguments, contractIdenifier])
138+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
139+
factory.createObjectLiteralExpression([
140+
factory.createPropertyAssignment('method', node.arguments[0]),
141+
factory.createPropertyAssignment('contract', contractIdenifier),
142+
]),
143+
])
144+
} else {
145+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
146+
factory.createObjectLiteralExpression([factory.createPropertyAssignment('method', node.arguments[0])]),
147+
])
132148
}
133-
return node
134149
},
135150

136151
callAbiCallFunction(node: ts.CallExpression, typeParams: ptypes.PType[]) {

src/test-transformer/visitors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ class ExpressionVisitor {
213213

214214
if (stubbedFunctionName) {
215215
if (isCallingMethodSelector(stubbedFunctionName)) {
216-
updatedNode = nodeFactory.callMethodSelectorFunction(updatedNode)
216+
const typeParams = this.helper.resolveTypeParameters(updatedNode)
217+
updatedNode = nodeFactory.callMethodSelectorFunction(updatedNode, typeParams)
217218
} else if (isCallingAbiCall(stubbedFunctionName)) {
218219
const typeParams = this.helper.resolveTypeParameters(updatedNode)
219220
updatedNode = nodeFactory.callAbiCallFunction(updatedNode, typeParams)

tests/arc4/method-selector.algo.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ describe('methodSelector', async () => {
4949
.map((_, i) => txn.appArgs(i))
5050
expect(appArgs).toEqual([appClient.getABIMethod('sink(string,uint8[])void').getSelector(), arg1.bytes, arg2.bytes])
5151
expect(appArgs[0]).toEqual(arc4.methodSelector(SignaturesContract.prototype.sink))
52+
expect(appArgs[0]).toEqual(arc4.methodSelector<typeof SignaturesContract.prototype.sink>())
53+
expect(appArgs[0]).toEqual(arc4.methodSelector('sink(string,uint8[])void'))
5254
})
5355

5456
test('app args is correct with alias', async ({ appClientSignaturesContract: appClient }) => {
@@ -71,6 +73,8 @@ describe('methodSelector', async () => {
7173
.map((_, i) => txn.appArgs(i))
7274
expect(appArgs).toEqual([appClient.getABIMethod('alias(string,uint8[])void').getSelector(), arg1.bytes, arg2.bytes])
7375
expect(appArgs[0]).toEqual(arc4.methodSelector(SignaturesContract.prototype.sink2))
76+
expect(appArgs[0]).toEqual(arc4.methodSelector<typeof SignaturesContract.prototype.sink2>())
77+
expect(appArgs[0]).toEqual(arc4.methodSelector('alias(string,uint8[])void'))
7478
})
7579

7680
test('app args is correct with txn', async ({ appClientSignaturesContract: appClient, algorand }) => {
@@ -99,6 +103,8 @@ describe('methodSelector', async () => {
99103
.map((_, i) => txn.appArgs(i))
100104
expect(appArgs).toEqual([appClient.getABIMethod('withTxn(string,pay,uint8[])void').getSelector(), arg1.bytes, arg3.bytes])
101105
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.withTxn))
106+
expect(appArgs[0]).toEqual(methodSelector<typeof SignaturesContract.prototype.withTxn>())
107+
expect(appArgs[0]).toEqual(methodSelector('withTxn(string,pay,uint8[])void'))
102108
})
103109

104110
test('app args is correct with asset', async ({ appClientSignaturesContract: appClient, algorand }) => {
@@ -133,6 +139,8 @@ describe('methodSelector', async () => {
133139
arg3.bytes,
134140
])
135141
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.withAsset))
142+
expect(appArgs[0]).toEqual(methodSelector<typeof SignaturesContract.prototype.withAsset>())
143+
expect(appArgs[0]).toEqual(methodSelector('withAsset(string,asset,uint8[])void'))
136144
})
137145

138146
test('app args is correct with account', async ({ appClientSignaturesContract: appClient, algorand }) => {
@@ -166,6 +174,8 @@ describe('methodSelector', async () => {
166174
arg3.bytes,
167175
])
168176
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.withAcc))
177+
expect(appArgs[0]).toEqual(methodSelector<typeof SignaturesContract.prototype.withAcc>())
178+
expect(appArgs[0]).toEqual(methodSelector('withAcc(string,account,uint8[])void'))
169179
})
170180

171181
test('app args is correct with application', async ({ appClientSignaturesContract: appClient, appFactorySignaturesContract }) => {
@@ -201,6 +211,8 @@ describe('methodSelector', async () => {
201211
arg4.bytes,
202212
])
203213
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.withApp))
214+
expect(appArgs[0]).toEqual(methodSelector<typeof SignaturesContract.prototype.withApp>())
215+
expect(appArgs[0]).toEqual(methodSelector('withApp(string,application,uint64,uint8[])void'))
204216
expect(appForeignApps.map((a) => a.id)).toEqual([selfApp.id, otherAppId])
205217
})
206218

@@ -246,6 +258,12 @@ describe('methodSelector', async () => {
246258
five.bytes,
247259
])
248260
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.complexSig))
261+
expect(appArgs[0]).toEqual(methodSelector<typeof SignaturesContract.prototype.complexSig>())
262+
expect(appArgs[0]).toEqual(
263+
methodSelector(
264+
'complexSig(((uint64,string),(uint64,string),uint128,uint128),pay,account,uint8[])((uint64,string),((uint64,string),(uint64,string),uint128,uint128))',
265+
),
266+
)
249267
expect(result[0].bytes).toEqual(struct.anotherStruct.bytes)
250268
expect(result[1].bytes).toEqual(struct.bytes)
251269
})
@@ -296,6 +314,12 @@ describe('methodSelector', async () => {
296314
Bytes.fromHex('01'),
297315
])
298316
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.echoResourceByIndex))
317+
expect(appArgs[0]).toEqual(methodSelector<typeof SignaturesContract.prototype.echoResourceByIndex>())
318+
expect(appArgs[0]).toEqual(
319+
methodSelector(
320+
'echoResourceByIndex(asset,application,account)(uint64,uint64,address)',
321+
),
322+
)
299323

300324
expect(result).toEqual([asaId, otherAppId, encodeAddress(acc.publicKey)])
301325
})
@@ -346,6 +370,12 @@ describe('methodSelector', async () => {
346370
toBytes(account),
347371
])
348372
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.echoResourceByValue))
373+
expect(appArgs[0]).toEqual(methodSelector<typeof SignaturesContract.prototype.echoResourceByValue>())
374+
expect(appArgs[0]).toEqual(
375+
methodSelector(
376+
'echoResourceByValue(uint64,uint64,address)(uint64,uint64,address)',
377+
),
378+
)
349379

350380
expect(result).toEqual([asaId, otherAppId, encodeAddress(acc.publicKey)])
351381
})
@@ -404,5 +434,11 @@ describe('methodSelector', async () => {
404434
five.bytes,
405435
])
406436
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.complexSig))
437+
expect(appArgs[0]).toEqual(methodSelector<typeof SignaturesContract.prototype.complexSig>())
438+
expect(appArgs[0]).toEqual(
439+
methodSelector(
440+
'complexSig(((uint64,string),(uint64,string),uint128,uint128),pay,account,uint8[])((uint64,string),((uint64,string),(uint64,string),uint128,uint128))',
441+
),
442+
)
407443
})
408444
})

0 commit comments

Comments
 (0)