Skip to content

Commit fcbf473

Browse files
committed
feat: add new overloads for begin and next methods of ItxnCompose to support type only import
1 parent 6628bf0 commit fcbf473

File tree

8 files changed

+85
-54
lines changed

8 files changed

+85
-54
lines changed

src/abi-metadata.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { arc4, OnCompleteActionStr } from '@algorandfoundation/algorand-typescript'
22
import js_sha512 from 'js-sha512'
33
import { ConventionalRouting } from './constants'
4+
import { InternalError } from './errors'
45
import { Arc4MethodConfigSymbol, Contract } from './impl/contract'
56
import type { TypeInfo } from './impl/encoded-types'
67
import { getArc4TypeName } from './impl/encoded-types'
7-
import type { DeliberateAny } from './typescript-helpers'
8+
import type { DeliberateAny, InstanceMethod } from './typescript-helpers'
89

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

113+
/** @internal */
114+
export const getContractMethod = (contractFullName: string, methodName: string) => {
115+
const contract = getContractByName(contractFullName)
116+
117+
if (contract === undefined || typeof contract !== 'function') {
118+
throw new InternalError(`Unknown contract: ${contractFullName}`)
119+
}
120+
121+
if (!Object.hasOwn(contract.prototype, methodName)) {
122+
throw new InternalError(`Unknown method: ${methodName} in contract: ${contractFullName}`)
123+
}
124+
125+
return {
126+
method: contract.prototype[methodName] as InstanceMethod<Contract, DeliberateAny[]>,
127+
contract,
128+
}
129+
}
130+
112131
/**
113132
* Get routing properties inferred by conventional naming
114133
* @param methodName The name of the method

src/impl/c2c.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import type {
44
ContractProxy,
55
TypedApplicationCallFields,
66
} from '@algorandfoundation/algorand-typescript/arc4'
7-
import { getContractByName, getContractMethodAbiMetadata } from '../abi-metadata'
7+
import { getContractMethod, getContractMethodAbiMetadata } from '../abi-metadata'
88
import { lazyContext } from '../context-helpers/internal-context'
9-
import { InternalError } from '../errors'
109
import type { ConstructorFor, DeliberateAny, InstanceMethod } from '../typescript-helpers'
1110
import type { ApplicationCallInnerTxn } from './inner-transactions'
1211
import { ApplicationCallInnerTxnContext } from './inner-transactions'
@@ -111,11 +110,9 @@ export function abiCall<TArgs extends DeliberateAny[], TReturn>(
111110
method: string,
112111
methodArgs: TypedApplicationCallFields<TArgs>,
113112
): { itxn: ApplicationCallInnerTxn; returnValue: TReturn | undefined } {
114-
const contract = getContractByName(contractFullName)
115-
if (contract === undefined || typeof contract !== 'function') throw new InternalError(`Unknown contract: ${contractFullName}`)
116-
if (!Object.hasOwn(contract.prototype, method)) throw new InternalError(`Unknown method: ${method} in contract: ${contractFullName}`)
113+
const { method: methodInstance, contract: contractInstance } = getContractMethod(contractFullName, method)
117114

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

120117
invokeAbiCall(itxnContext)
121118

src/impl/itxn-compose.ts

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import type {
1010
KeyRegistrationComposeFields,
1111
PaymentComposeFields,
1212
} from '@algorandfoundation/algorand-typescript'
13-
import type { TypedApplicationCallFields } from '@algorandfoundation/algorand-typescript/arc4'
13+
import type { AbiCallOptions, TypedApplicationCallFields } from '@algorandfoundation/algorand-typescript/arc4'
14+
import { getContractMethod } from '../abi-metadata'
1415
import { lazyContext } from '../context-helpers/internal-context'
1516
import type { DeliberateAny, InstanceMethod } from '../typescript-helpers'
1617
import { getApplicationCallInnerTxnContext } from './c2c'
@@ -29,16 +30,9 @@ class ItxnCompose {
2930
fields: TypedApplicationCallFields<TArgs>,
3031
contract?: Contract | { new (): Contract },
3132
): void
32-
begin<TArgs extends DeliberateAny[]>(...args: unknown[]): void {
33-
lazyContext.txn.activeGroup.constructingItxnGroup.push(
34-
args.length === 1
35-
? (args[0] as AnyTransactionComposeFields)
36-
: getApplicationCallInnerTxnContext(
37-
args[0] as InstanceMethod<Contract, TArgs>,
38-
args[1] as TypedApplicationCallFields<TArgs>,
39-
args[2] as Contract | { new (): Contract },
40-
),
41-
)
33+
begin<TMethod>(options: AbiCallOptions<TMethod>, contract: string, method: string): void
34+
begin(...args: unknown[]): void {
35+
this.addInnerTransaction(...args)
4236
}
4337

4438
next(fields: PaymentComposeFields): void
@@ -49,22 +43,49 @@ class ItxnCompose {
4943
next(fields: ApplicationCallComposeFields): void
5044
next(fields: AnyTransactionComposeFields): void
5145
next(fields: ComposeItxnParams): void
52-
next<TArgs extends DeliberateAny[]>(_method: InstanceMethod<Contract, TArgs>, _fields: TypedApplicationCallFields<TArgs>): void
53-
next<TArgs extends DeliberateAny[]>(...args: unknown[]): void {
54-
lazyContext.txn.activeGroup.constructingItxnGroup.push(
55-
args.length === 1
56-
? (args[0] as AnyTransactionComposeFields)
57-
: getApplicationCallInnerTxnContext(
58-
args[0] as InstanceMethod<Contract, TArgs>,
59-
args[1] as TypedApplicationCallFields<TArgs>,
60-
args[2] as Contract | { new (): Contract },
61-
),
62-
)
46+
next<TArgs extends DeliberateAny[]>(
47+
_method: InstanceMethod<Contract, TArgs>,
48+
_fields: TypedApplicationCallFields<TArgs>,
49+
contract?: Contract | { new (): Contract },
50+
): void
51+
next<TMethod>(options: AbiCallOptions<TMethod>, contract: string, method: string): void
52+
next(...args: unknown[]): void {
53+
this.addInnerTransaction(...args)
6354
}
6455

6556
submit(): void {
6657
lazyContext.txn.activeGroup.submitInnerTransactionGroup()
6758
}
59+
60+
private addInnerTransaction<TArgs extends DeliberateAny[]>(...args: unknown[]): void {
61+
let innerTxnFields
62+
63+
// Single argument: direct transaction fields
64+
if (args.length === 1) {
65+
innerTxnFields = args[0] as AnyTransactionComposeFields
66+
}
67+
// Three arguments with object fields (deprecated signature):
68+
// e.g. `itxnCompose.begin(Hello.prototype.greet, { appId, args: ['ho'] })`
69+
else if (args.length === 3 && typeof args[1] === 'object') {
70+
innerTxnFields = getApplicationCallInnerTxnContext(
71+
args[0] as InstanceMethod<Contract, TArgs>,
72+
args[1] as TypedApplicationCallFields<TArgs>,
73+
args[2] as Contract | { new (): Contract },
74+
)
75+
}
76+
// Three arguments with string contract name:
77+
// e.g. `itxnCompose.next({ method: Hello.prototype.greet, appId, args: ['ho'] })`
78+
// or `itxnCompose.next<typeof Hello.prototype.greet>({ appId, args: ['ho'] })`
79+
else {
80+
const contractFullName = args[1] as string
81+
const methodName = args[2] as string
82+
const { method, contract } = getContractMethod(contractFullName, methodName)
83+
84+
innerTxnFields = getApplicationCallInnerTxnContext(method, args[0] as TypedApplicationCallFields<TArgs>, contract)
85+
}
86+
87+
lazyContext.txn.activeGroup.constructingItxnGroup.push(innerTxnFields)
88+
}
6889
}
6990

7091
/** @internal */

src/impl/method-selector.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { bytes } from '@algorandfoundation/algorand-typescript'
22
import { encodingUtil } from '@algorandfoundation/puya-ts'
3-
import { getArc4Selector, getContractByName, getContractMethodAbiMetadata } from '../abi-metadata'
4-
import { CodeError, InternalError } from '../errors'
3+
import { getArc4Selector, getContractMethod, getContractMethodAbiMetadata } from '../abi-metadata'
4+
import { CodeError } from '../errors'
55
import type { InstanceMethod } from '../typescript-helpers'
66
import type { Contract } from './contract'
77
import { sha512_256 } from './crypto'
@@ -39,15 +39,7 @@ export const methodSelector = <TContract extends Contract>({
3939

4040
// Pattern 2: Contract name as string with method name
4141
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-
}
42+
const { contract: registeredContract } = getContractMethod(contract, method)
5143

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

src/test-transformer/node-factory.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,23 @@ export const nodeFactory = {
159159
return node
160160
},
161161

162-
callItxnComposeFunction(node: ts.CallExpression) {
162+
callItxnComposeFunction(node: ts.CallExpression, typeParams: ptypes.PType[]) {
163163
if (
164164
node.arguments.length === 2 &&
165165
ts.isPropertyAccessExpression(node.arguments[0]) &&
166166
ts.isPropertyAccessExpression(node.arguments[0].expression)
167167
) {
168168
const contractIdenifier = node.arguments[0].expression.expression
169169
return factory.updateCallExpression(node, node.expression, node.typeArguments, [...node.arguments, contractIdenifier])
170+
} else if (
171+
node.arguments.length === 1 &&
172+
typeParams.length === 1 &&
173+
typeParams[0] instanceof ptypes.FunctionPType &&
174+
typeParams[0].declaredIn
175+
) {
176+
const contractIdentifier = factory.createStringLiteral(typeParams[0].declaredIn.fullName)
177+
const methodName = factory.createStringLiteral(typeParams[0].name)
178+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [...node.arguments, contractIdentifier, methodName])
170179
}
171180
return node
172181
},

src/test-transformer/visitors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ class ExpressionVisitor {
219219
const typeParams = this.helper.resolveTypeParameters(updatedNode)
220220
updatedNode = nodeFactory.callAbiCallFunction(updatedNode, typeParams)
221221
} else if (isCallingItxnCompose(stubbedFunctionName)) {
222-
updatedNode = nodeFactory.callItxnComposeFunction(updatedNode)
222+
const typeParams = this.helper.resolveTypeParameters(updatedNode)
223+
updatedNode = nodeFactory.callItxnComposeFunction(updatedNode, typeParams)
223224
} else {
224225
updatedNode = nodeFactory.callStubbedFunction(updatedNode, infoArg)
225226
}

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,7 @@ describe('methodSelector', async () => {
315315
])
316316
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.echoResourceByIndex))
317317
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-
)
318+
expect(appArgs[0]).toEqual(methodSelector('echoResourceByIndex(asset,application,account)(uint64,uint64,address)'))
323319

324320
expect(result).toEqual([asaId, otherAppId, encodeAddress(acc.publicKey)])
325321
})
@@ -371,11 +367,7 @@ describe('methodSelector', async () => {
371367
])
372368
expect(appArgs[0]).toEqual(methodSelector(SignaturesContract.prototype.echoResourceByValue))
373369
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-
)
370+
expect(appArgs[0]).toEqual(methodSelector('echoResourceByValue(uint64,uint64,address)(uint64,uint64,address)'))
379371

380372
expect(result).toEqual([asaId, otherAppId, encodeAddress(acc.publicKey)])
381373
})

tests/artifacts/itxn-compose/contract.algo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class ItxnComposeAlgo extends Contract {
5959
})
6060
}
6161

62-
itxnCompose.next(VerifierContract.prototype.verify, {
62+
itxnCompose.next<typeof VerifierContract.prototype.verify>({
6363
appId: verifier,
6464
})
6565

@@ -79,7 +79,7 @@ export class ItxnComposeAlgo extends Contract {
7979
if (i === 0) {
8080
itxnCompose.begin(Hello.prototype.greet, { appId, args: ['ho'] })
8181
} else {
82-
itxnCompose.next(Hello.prototype.greet, { appId, args: ['ho'] })
82+
itxnCompose.next({ method: Hello.prototype.greet, appId, args: ['ho'] })
8383
}
8484
}
8585
itxnCompose.submit()

0 commit comments

Comments
 (0)