Skip to content

Commit bd81564

Browse files
committed
feat: Add generic version of itxnCompose.begin and next, plus methodSelector helper which works with type only imports
1 parent 9aa19a2 commit bd81564

File tree

121 files changed

+6212
-794
lines changed

Some content is hidden

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

121 files changed

+6212
-794
lines changed

packages/algo-ts/src/arc4/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,16 @@ export function methodSelector(methodSignature: InstanceMethod<Contract>): bytes
216216
* @returns The ARC4 method selector. Eg. `02BECE11`
217217
*/
218218
export function methodSelector(methodSignature: string): bytes<4>
219-
export function methodSelector(methodSignature: string | InstanceMethod<Contract>): bytes<4> {
219+
/**
220+
* Returns the ARC4 method selector for a given ARC4 method signature. The method selector is the first
221+
* 4 bytes of the SHA512/256 hash of the method signature.
222+
* @typeParam TMethod The type of an ARC4 method signature (eg. `typeof MyContract.prototype.myMethod`)
223+
* @returns The ARC4 method selector. Eg. `02BECE11`
224+
* @remarks This overload can be used in conjunction with type only import (eg. `import type { MyContract } from './my-contract'`) to
225+
* work around what would otherwise be a circular reference in the event two contracts need to call each other.
226+
*/
227+
export function methodSelector<TMethod>(): bytes<4>
228+
export function methodSelector<TMethod>(methodSignature?: string | InstanceMethod<Contract>): bytes<4> {
220229
throw new NoImplementation()
221230
}
222231

packages/algo-ts/src/itxn-compose.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Contract, TypedApplicationCallFields } from './arc4'
1+
import { AbiCallOptions, Contract, TypedApplicationCallFields } from './arc4'
22
import { NoImplementation } from './internal/errors'
33
import { DeliberateAny, InstanceMethod } from './internal/typescript-helpers'
44
import { itxn } from './itxn'
@@ -42,27 +42,138 @@ export type ComposeItxnParams =
4242
| itxn.ApplicationCallItxnParams
4343

4444
export type ItxnCompose = {
45+
/**
46+
* Begin a transaction group with a payment transaction
47+
* @param fields Specifies any transaction fields which should differ from their defaults
48+
*/
4549
begin(fields: PaymentComposeFields): void
50+
/**
51+
* Begin a transaction group with a key registration transaction
52+
* @param fields Specifies any transaction fields which should differ from their defaults
53+
*/
4654
begin(fields: KeyRegistrationComposeFields): void
55+
/**
56+
* Begin a transaction group with an asset config transaction
57+
* @param fields Specifies any transaction fields which should differ from their defaults
58+
*/
4759
begin(fields: AssetConfigComposeFields): void
60+
/**
61+
* Begin a transaction group with an asset transfer transaction
62+
* @param fields Specifies any transaction fields which should differ from their defaults
63+
*/
4864
begin(fields: AssetTransferComposeFields): void
65+
/**
66+
* Begin a transaction group with an asset freeze transaction
67+
* @param fields Specifies any transaction fields which should differ from their defaults
68+
*/
4969
begin(fields: AssetFreezeComposeFields): void
70+
/**
71+
* Begin a transaction group with an application call transaction
72+
* @param fields Specifies any transaction fields which should differ from their defaults
73+
*/
5074
begin(fields: ApplicationCallComposeFields): void
75+
/**
76+
* Begin a transaction group with a new transaction with the specified fields
77+
* @param fields Specifies the type, and any transaction fields which should differ from their defaults
78+
*/
5179
begin(fields: AnyTransactionComposeFields): void
80+
/**
81+
* Begin a transaction group with a new transaction from the specified itxn params object
82+
* @param fields
83+
*/
5284
begin(fields: ComposeItxnParams): void
85+
/**
86+
* Begin a transaction group with a typed application call transaction.
87+
* @param method The ABI method to call
88+
* @param fields Specifies any transaction fields which should differ from their defaults
89+
*
90+
* @deprecated This overload has been deprecated in favour of the single arg overload where method is specified as a property of the fields
91+
* object, or via an explicit generic param. (`itxnCompose.begin({ method: MyContract.prototype.myMethod, ... })` or
92+
* `itxnCompose.begin<typeof MyContract.prototype.myMethod>({ ... })`)
93+
*/
5394
begin<TArgs extends DeliberateAny[]>(method: InstanceMethod<Contract, TArgs>, fields: TypedApplicationCallFields<TArgs>): void
95+
/**
96+
* Begin a transaction group with a typed application call transaction. The method can be specified by options.method, or
97+
* by explicitly defining the type of the generic parameter TMethod.
98+
* @param options Specifies any transaction fields which should differ from their defaults
99+
* @typeParam TMethod The type of an ARC4 method signature (eg. `typeof MyContract.prototype.myMethod`)
100+
*/
101+
begin<TMethod>(options: AbiCallOptions<TMethod>): void
54102

103+
/**
104+
* Continue a transaction group with a payment transaction
105+
* @param fields Specifies any transaction fields which should differ from their defaults
106+
*/
55107
next(fields: PaymentComposeFields): void
108+
/**
109+
* Continue a transaction group with a key registration transaction
110+
* @param fields Specifies any transaction fields which should differ from their defaults
111+
*/
56112
next(fields: KeyRegistrationComposeFields): void
113+
/**
114+
* Continue a transaction group with an asset config transaction
115+
* @param fields Specifies any transaction fields which should differ from their defaults
116+
*/
57117
next(fields: AssetConfigComposeFields): void
118+
/**
119+
* Continue a transaction group with an asset transfer transaction
120+
* @param fields Specifies any transaction fields which should differ from their defaults
121+
*/
58122
next(fields: AssetTransferComposeFields): void
123+
/**
124+
* Continue a transaction group with an asset freeze transaction
125+
* @param fields Specifies any transaction fields which should differ from their defaults
126+
*/
59127
next(fields: AssetFreezeComposeFields): void
128+
/**
129+
* Continue a transaction group with an application call transaction
130+
* @param fields Specifies any transaction fields which should differ from their defaults
131+
*/
60132
next(fields: ApplicationCallComposeFields): void
133+
/**
134+
* Continue a transaction group with a new transaction with the specified fields
135+
* @param fields Specifies the type, and any transaction fields which should differ from their defaults
136+
*/
61137
next(fields: AnyTransactionComposeFields): void
138+
/**
139+
* Continue a transaction group with a new transaction from the specified itxn params object
140+
* @param fields
141+
*/
62142
next(fields: ComposeItxnParams): void
143+
/**
144+
* Continue a transaction group with a typed application call transaction.
145+
* @param method The ABI method to call
146+
* @param fields Specifies any transaction fields which should differ from their defaults
147+
*
148+
* @deprecated This overload has been deprecated in favour of the single arg overload where method is specified as a property of the fields
149+
* object, or via an explicit generic param. (`itxnCompose.next({ method: MyContract.prototype.myMethod, ... })` or
150+
* `itxnCompose.next<typeof MyContract.prototype.myMethod>({ ... })`)
151+
*/
63152
next<TArgs extends DeliberateAny[]>(method: InstanceMethod<Contract, TArgs>, fields: TypedApplicationCallFields<TArgs>): void
153+
/**
154+
* Continue a transaction group with a typed application call transaction. The method can be specified by options.method, or
155+
* by explicitly defining the type of the generic parameter TMethod.
156+
* @param options Specifies any transaction fields which should differ from their defaults
157+
* @typeParam TMethod The type of an ARC4 method signature (eg. `typeof MyContract.prototype.myMethod`)
158+
*/
159+
next<TMethod>(options: AbiCallOptions<TMethod>): void
64160

161+
/**
162+
* Submit all transactions in the group
163+
*
164+
* @remarks `op.GITxn.lastLog(n)` (and other methods on the GITxn object) can be used to read fields from the most recently submitted
165+
* transaction group where `n` is a compile time constant representing the index of the transaction in the group.
166+
*/
65167
submit(): void
66168
}
67169

170+
/**
171+
* The itxnCompose helper can be used to build dynamically sized itxn groups which aren't supported by the stronger typed itxn paradigm. The
172+
* first transaction in a group must be 'staged' with `itxnCompose.begin` whilst all other transactions in the group should use `itxnCompose.next`.
173+
* When the group is complete it can be submitted using `itxnCompose.submit`.
174+
*
175+
* @remarks The itxn API offered by teal opcodes has some rough edges which are not fully abstracted over by this compose API, but it hoped that use
176+
* cases for it are limited and that most transaction groups can be composed with a static size relying on the atomic nature of the outer transaction
177+
* to ensure multiple smaller itxn groups are committed atomically.
178+
*/
68179
export const itxnCompose: ItxnCompose = NoImplementation.value()

src/awst_build/arc4-util.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { ResourceEncoding } from '../awst'
22
import { nodeFactory } from '../awst/node-factory'
3-
import type { ARC4ABIMethodConfig } from '../awst/nodes'
3+
import { ARC4ABIMethodConfig } from '../awst/nodes'
44
import type { SourceLocation } from '../awst/source-location'
55
import { wtypes } from '../awst/wtypes'
66
import { CodeError } from '../errors'
77
import { codeInvariant, invariant } from '../util'
8+
import { AwstBuildContext } from './context/awst-build-context'
89
import type { ABICompatiblePType, FunctionPType, PType } from './ptypes'
910
import {
1011
ABICompatibleInstanceType,
@@ -84,6 +85,29 @@ export function ptypeToAbiPType(
8485
throw new CodeError(`${ptype} cannot be used as an ABI ${direction === 'in' ? 'param' : 'return'} type`, { sourceLocation })
8586
}
8687

88+
export function arc4ConfigFromType(functionType: FunctionPType, sourceLocation: SourceLocation) {
89+
codeInvariant(
90+
functionType.declaredIn,
91+
`${functionType.name} does not appear to be a contract method. Ensure you are calling a function defined on a contract class eg. abiCall<typeof YourContract.prototype.yourMethod>`,
92+
sourceLocation,
93+
)
94+
95+
const contractType = AwstBuildContext.current.getContractTypeByName(functionType.declaredIn)
96+
invariant(contractType, `${functionType.declaredIn} has not been visited`)
97+
98+
const arc4Config = AwstBuildContext.current.getArc4Config(contractType, functionType.name)
99+
codeInvariant(
100+
arc4Config instanceof ARC4ABIMethodConfig,
101+
`${functionType.name} is not an ABI method. Only ABI compatible methods can be called with this helper.`,
102+
sourceLocation,
103+
)
104+
const methodSelector = buildArc4MethodConstant(functionType, arc4Config, sourceLocation)
105+
return {
106+
arc4Config,
107+
methodSelector,
108+
}
109+
}
110+
87111
/**
88112
* Generate a methodConstant node for the given function, making use of the ARC4ABIMethodConfig
89113
* @param functionType The function ptype

src/awst_build/eb/arc4/c2c.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Constants } from '../../../constants'
1010
import { logger } from '../../../logger'
1111
import { codeInvariant, enumFromValue, hexToUint8Array, invariant } from '../../../util'
1212
import { parseArc4Method } from '../../../util/arc4-signature-parser'
13-
import { buildArc4MethodConstant, ptypeToArc4EncodedType } from '../../arc4-util'
13+
import { arc4ConfigFromType, buildArc4MethodConstant, ptypeToArc4EncodedType } from '../../arc4-util'
1414
import { AwstBuildContext } from '../../context/awst-build-context'
1515
import type { PType } from '../../ptypes'
1616
import {
@@ -66,22 +66,7 @@ export class AbiCallFunctionBuilder extends FunctionBuilder {
6666
'Generic type variable TMethod must be a contract method. eg. abiCall<typeof YourContract.prototype.yourMethod>',
6767
sourceLocation,
6868
)
69-
codeInvariant(
70-
functionType.declaredIn,
71-
`${functionType.name} does not appear to be a contract method. Ensure you are calling a function defined on a contract class eg. abiCall<typeof YourContract.prototype.yourMethod>`,
72-
sourceLocation,
73-
)
74-
75-
const contractType = AwstBuildContext.current.getContractTypeByName(functionType.declaredIn)
76-
invariant(contractType, `${functionType.declaredIn} has not been visited`)
77-
78-
const arc4Config = AwstBuildContext.current.getArc4Config(contractType, functionType.name)
79-
codeInvariant(
80-
arc4Config instanceof ARC4ABIMethodConfig,
81-
`${functionType.name} is not an ABI method. Only ABI compatible methods can be called with this helper.`,
82-
sourceLocation,
83-
)
84-
const methodSelector = buildArc4MethodConstant(functionType, arc4Config, sourceLocation)
69+
const { methodSelector, arc4Config } = arc4ConfigFromType(functionType, sourceLocation)
8570

8671
const itxnResult = makeApplicationCall({
8772
fields: options,

src/awst_build/eb/arc4/util.ts

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import { Constants } from '../../../constants'
88
import { CodeError } from '../../../errors'
99
import { logger } from '../../../logger'
1010
import { codeInvariant, hexToUint8Array } from '../../../util'
11-
import { isArc4EncodableType, ptypeToArc4EncodedType } from '../../arc4-util'
11+
import { arc4ConfigFromType, isArc4EncodableType, ptypeToArc4EncodedType } from '../../arc4-util'
1212
import type { PType } from '../../ptypes'
13-
import { BytesPType, bytesPType, stringPType, uint64PType } from '../../ptypes'
13+
import { BytesPType, bytesPType, FunctionPType, stringPType, uint64PType } from '../../ptypes'
1414
import {
1515
ARC4EncodedType,
1616
convertBytesFunction,
@@ -202,39 +202,52 @@ export class MethodSelectorFunctionBuilder extends FunctionBuilder {
202202
readonly ptype = methodSelectorFunction
203203

204204
call(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation<ts.CallExpression>): NodeBuilder {
205-
const {
206-
args: [methodSignature],
207-
} = parseFunctionArgs({
208-
args,
209-
typeArgs,
210-
genericTypeArgs: 0,
211-
callLocation: sourceLocation,
212-
funcName: this.typeDescription,
213-
argSpec: (a) => [a.passthrough()],
214-
})
215205
const methodConstantType = new BytesPType({ length: 4n })
216-
if (methodSignature instanceof SubroutineExpressionBuilder) {
206+
207+
if (typeArgs.length === 1 && args.length === 0) {
208+
const [functionType] = typeArgs
217209
codeInvariant(
218-
methodSignature instanceof ContractMethodExpressionBuilder,
219-
`Expected contract instance method, found ${methodSignature.typeDescription}`,
220-
methodSignature.sourceLocation,
210+
functionType instanceof FunctionPType,
211+
'Generic type variable TMethod must be a contract method. eg. abiCall<typeof YourContract.prototype.yourMethod>',
212+
sourceLocation,
221213
)
222-
return instanceEb(methodSignature.getMethodSelector(sourceLocation), methodConstantType)
214+
const { methodSelector } = arc4ConfigFromType(functionType, sourceLocation)
215+
return instanceEb(methodSelector, methodConstantType)
223216
} else {
224-
if (methodSignature === undefined) {
225-
throw new CodeError(
226-
`${this.typeDescription} expects exactly 1 argument that is either a string literal, or a contract function reference`,
227-
{ sourceLocation },
217+
const {
218+
args: [methodSignature],
219+
} = parseFunctionArgs({
220+
args,
221+
typeArgs,
222+
genericTypeArgs: 0,
223+
callLocation: sourceLocation,
224+
funcName: this.typeDescription,
225+
argSpec: (a) => [a.passthrough()],
226+
})
227+
228+
if (methodSignature instanceof SubroutineExpressionBuilder) {
229+
codeInvariant(
230+
methodSignature instanceof ContractMethodExpressionBuilder,
231+
`Expected contract instance method, found ${methodSignature.typeDescription}`,
232+
methodSignature.sourceLocation,
233+
)
234+
return instanceEb(methodSignature.getMethodSelector(sourceLocation), methodConstantType)
235+
} else {
236+
if (methodSignature === undefined) {
237+
throw new CodeError(
238+
`${this.typeDescription} expects exactly 1 argument that is either a string literal, or a contract function reference`,
239+
{ sourceLocation },
240+
)
241+
}
242+
return instanceEb(
243+
nodeFactory.methodConstant({
244+
value: requireStringConstant(methodSignature).value,
245+
wtype: methodConstantType.wtype,
246+
sourceLocation,
247+
}),
248+
methodConstantType,
228249
)
229250
}
230-
return instanceEb(
231-
nodeFactory.methodConstant({
232-
value: requireStringConstant(methodSignature).value,
233-
wtype: methodConstantType.wtype,
234-
sourceLocation,
235-
}),
236-
methodConstantType,
237-
)
238251
}
239252
}
240253
}

0 commit comments

Comments
 (0)