Skip to content

Commit

Permalink
feat: ARC28 emit function
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanmenzel committed Dec 5, 2024
1 parent e0cce03 commit d0bfed7
Show file tree
Hide file tree
Showing 23 changed files with 3,656 additions and 21 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dev:examples": "tsx src/cli.ts build examples --output-awst --output-awst-json",
"dev:approvals": "rimraf tests/approvals/out && tsx src/cli.ts build tests/approvals --dry-run",
"dev:expected-output": "tsx src/cli.ts build tests/expected-output --dry-run",
"dev:testing": "tsx src/cli.ts build tests/approvals/precompiled-factory.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --out-dir out/[name] --optimization-level=0",
"dev:testing": "tsx src/cli.ts build tests/approvals/arc28-events.algo.ts --output-awst --output-awst-json --output-ssa-ir --log-level=info --out-dir out/[name] --optimization-level=0",
"audit": "better-npm-audit audit",
"format": "prettier --write .",
"lint": "eslint \"src/**/*.ts\"",
Expand Down
39 changes: 39 additions & 0 deletions packages/algo-ts/src/arc-28.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NoImplementation } from './impl/errors'
import { DeliberateAny } from './typescript-helpers'

/**
* Emit an arc28 event log using either an ARC4Struct type or a named object type.
* Object types must have an ARC4 equivalent type.
*
* Anonymous types cannot be used as the type name is used to determine the event prefix
* @param event An ARC4Struct instance, or a plain object with a named type
*
* @example
* class Demo extends Struct<{ a: UintN64 }> {}
* emit(new Demo({ a: new UintN64(123) }))
*
* @example
* type Demo = { a: uint64 }
* emit<Demo>({a: 123})
* // or
* const d: Demo = { a: 123 }
* emit(d)
*/
export function emit<TEvent extends Record<string, DeliberateAny>>(event: TEvent): void
/**
* Emit an arc28 event log using an explicit name and inferred property/field types.
* Property types must be ARC4 or have an ARC4 equivalent type.
* @param eventName The name of the event (must be a compile time constant)
* @param eventProps A set of event properties (order is significant)
*
* @example
* emit("Demo", new UintN64(123))
*
* @example
* const a: uint64 = 123
* emit("Demo", a)
*/
export function emit<TProps extends [...DeliberateAny[]]>(eventName: string, ...eventProps: TProps): void
export function emit<T>(event: T | string, ...eventProps: unknown[]): void {
throw new NoImplementation()
}
1 change: 1 addition & 0 deletions packages/algo-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export { TemplateVar } from './template-var'
export { Base64, Ec, Ecdsa, VrfVerify } from './op-types'
export { compile, CompiledContract, CompiledLogicSig } from './compiled'
export { MutableArray } from './mutable-array'
export { emit } from './arc-28'
2 changes: 1 addition & 1 deletion src/awst/to-code-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export class ToCodeVisitor
]
}
visitEmit(expression: Emit): string {
throw new TodoError('Method not implemented.', { sourceLocation: expression.sourceLocation })
return `emit("${expression.signature}", ${expression.value.accept(this)})`
}

visitContractMethod(statement: nodes.ContractMethod): string[] {
Expand Down
9 changes: 7 additions & 2 deletions src/awst/wtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,9 @@ export namespace wtypes {
sourceLocation?: SourceLocation
}) {
super({
arc4Name: Object.values(fields)
arc4Name: `(${Object.values(fields)
.map((f) => f.arc4Name)
.join(','),
.join(',')})`,
name,
nativeType: null,
})
Expand All @@ -278,6 +278,11 @@ export namespace wtypes {
this.frozen = frozen
this.desc = desc
}

toString(): string {
if (!this.name) return this.arc4Name
return super.toString()
}
}
export class ARC4Tuple extends ARC4Type {
readonly types: ARC4Type[]
Expand Down
8 changes: 8 additions & 0 deletions src/awst_build/arc4-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
boolPType,
bytesPType,
GroupTransactionPType,
NativeNumericType,
numberPType,
ObjectPType,
stringPType,
TuplePType,
Expand Down Expand Up @@ -51,13 +53,19 @@ export function isArc4EncodableType(ptype: PType): boolean {

return false
}
export function ptypeToArc4EncodedType(ptype: TuplePType, sourceLocation: SourceLocation): ARC4TupleType
export function ptypeToArc4EncodedType(ptype: ObjectPType, sourceLocation: SourceLocation): ARC4StructType
export function ptypeToArc4EncodedType(ptype: PType, sourceLocation: SourceLocation): ARC4EncodedType
export function ptypeToArc4EncodedType(ptype: PType, sourceLocation: SourceLocation): ARC4EncodedType {
if (ptype instanceof ARC4EncodedType) return ptype
if (ptype.equals(boolPType)) return ARC4BooleanType
if (ptype.equals(uint64PType)) return new UintNType({ n: 64n })
if (ptype.equals(biguintPType)) return new UintNType({ n: 512n })
if (ptype.equals(bytesPType)) return DynamicBytesType
if (ptype.equals(stringPType)) return ARC4StringType
if (ptype instanceof NativeNumericType) {
throw new CodeError(numberPType.expressionMessage, { sourceLocation })
}
if (ptype instanceof TuplePType) return new ARC4TupleType({ types: ptype.items.map((i) => ptypeToArc4EncodedType(i, sourceLocation)) })
if (ptype instanceof ObjectPType)
return new ARC4StructType({
Expand Down
109 changes: 109 additions & 0 deletions src/awst_build/eb/arc28/arc-28-emit-function-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { nodeFactory } from '../../../awst/node-factory'
import type { Expression } from '../../../awst/nodes'
import type { SourceLocation } from '../../../awst/source-location'
import { InternalError } from '../../../errors'
import { logger } from '../../../logger'
import { codeInvariant } from '../../../util'
import { ptypeToArc4EncodedType } from '../../arc4-util'
import type { PType } from '../../ptypes'
import { arc28EmitFunction, ObjectPType, stringPType, voidPType } from '../../ptypes'
import { ARC4EncodedType, ARC4StructType } from '../../ptypes/arc4-types'
import { instanceEb } from '../../type-registry'
import type { NodeBuilder } from '../index'
import { FunctionBuilder } from '../index'
import { requireStringConstant } from '../util'
import { parseFunctionArgs } from '../util/arg-parsing'

export class Arc28EmitFunctionBuilder extends FunctionBuilder {
readonly ptype = arc28EmitFunction

call(args: ReadonlyArray<NodeBuilder>, typeArgs: ReadonlyArray<PType>, sourceLocation: SourceLocation): NodeBuilder {
const {
args: [nameOrObj, ...props],
ptypes: [genericArg],
} = parseFunctionArgs({
args,
typeArgs,
genericTypeArgs: 1,
callLocation: sourceLocation,
funcName: this.typeDescription,
argSpec: (a) => [a.required(), ...args.slice(1).map(() => a.required())],
})

if (nameOrObj.ptype.equals(stringPType)) {
const name = requireStringConstant(nameOrObj).value
const thisModule = nameOrObj.sourceLocation.file ?? ''

const fields: Record<string, ARC4EncodedType> = {}
const values = new Map<string, Expression>()

for (const [index, prop] of Object.entries(props)) {
const arc4Type = ptypeToArc4EncodedType(prop.ptype, prop.sourceLocation)
fields[index] = arc4Type
values.set(
index,
prop.ptype instanceof ARC4EncodedType
? prop.resolve()
: nodeFactory.aRC4Encode({
value: prop.resolve(),
wtype: arc4Type.wtype,
sourceLocation: prop.sourceLocation,
}),
)
}

const structType = new ARC4StructType({
name,
module: thisModule,
fields,
description: undefined,
sourceLocation,
})
const structExpression = nodeFactory.newStruct({
wtype: structType.wtype,
values,
sourceLocation,
})

return emitStruct(structType, structExpression, sourceLocation)
}
codeInvariant(props.length === 0, 'Unexpected args', props[0]?.sourceLocation)

const eventBuilder = nameOrObj.resolveToPType(genericArg)

const eventType = eventBuilder.ptype
if (eventType instanceof ARC4StructType) {
return emitStruct(eventType, nameOrObj.resolve(), sourceLocation)
} else if (eventType instanceof ObjectPType) {
if (eventType.isAnonymous) {
logger.error(
eventBuilder.sourceLocation,
'Event cannot be an anonymous type. If a named type exists, try specifying it explicitly via the generic parameter. Eg. `emit<YourType>({...})`',
)
}
const arc4Equivalent = ptypeToArc4EncodedType(eventType, sourceLocation)
return emitStruct(
arc4Equivalent,
nodeFactory.aRC4Encode({
wtype: arc4Equivalent.wtype,
sourceLocation: nameOrObj.sourceLocation,
value: nameOrObj.resolve(),
}),
sourceLocation,
)
}
throw new InternalError('Unexpected type for arg 0 of emit', { sourceLocation })
}
}

function emitStruct(ptype: ARC4StructType, expression: Expression, sourceLocation: SourceLocation) {
return instanceEb(
nodeFactory.emit({
signature: ptype.signature,
value: expression,
wtype: voidPType.wtype,
sourceLocation,
}),
voidPType,
)
}
30 changes: 15 additions & 15 deletions src/awst_build/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,11 @@ export function buildLibAwst(context: AwstBuildContext) {
propertyInitialization: [],
ctor: nodeFactory.contractMethod({
memberName: Constants.constructorMethodName,
cref: contractCref,
cref: baseContractCref,
args: [],
sourceLocation: SourceLocation.None,
documentation: nodeFactory.methodDocumentation(),
body: nodeFactory.block(
{ sourceLocation: SourceLocation.None },
nodeFactory.expressionStatement({
expr: nodeFactory.subroutineCallExpression({
args: [],
wtype: wtypes.voidWType,
target: nodeFactory.instanceMethodTarget({
memberName: Constants.constructorMethodName,
}),
sourceLocation: SourceLocation.None,
}),
}),
),
body: nodeFactory.block({ sourceLocation: SourceLocation.None }),
returnType: wtypes.voidWType,
arc4MethodConfig: null,
}),
Expand Down Expand Up @@ -75,7 +63,19 @@ export function buildLibAwst(context: AwstBuildContext) {
args: [],
sourceLocation: SourceLocation.None,
documentation: nodeFactory.methodDocumentation(),
body: nodeFactory.block({ sourceLocation: SourceLocation.None }),
body: nodeFactory.block(
{ sourceLocation: SourceLocation.None },
nodeFactory.expressionStatement({
expr: nodeFactory.subroutineCallExpression({
args: [],
wtype: wtypes.voidWType,
target: nodeFactory.instanceMethodTarget({
memberName: Constants.constructorMethodName,
}),
sourceLocation: SourceLocation.None,
}),
}),
),
returnType: wtypes.voidWType,
arc4MethodConfig: null,
}),
Expand Down
4 changes: 4 additions & 0 deletions src/awst_build/ptypes/arc4-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ export class ARC4StructType extends ARC4EncodedType {
})
}

get signature(): string {
return `${this.name}${this.wtype.arc4Name}`
}

equals(other: PType): boolean {
if (!(other instanceof ARC4StructType)) return false
const thisProps = Object.entries(this.fields)
Expand Down
5 changes: 5 additions & 0 deletions src/awst_build/ptypes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1289,3 +1289,8 @@ export const compiledLogicSigType = new ObjectPType({
account: accountPType,
},
})

export const arc28EmitFunction = new LibFunctionType({
name: 'emit',
module: Constants.arc28ModuleName,
})
3 changes: 3 additions & 0 deletions src/awst_build/ptypes/register.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Arc28EmitFunctionBuilder } from '../eb/arc28/arc-28-emit-function-builder'
import { Arc4AbiMethodDecoratorBuilder, Arc4BareMethodDecoratorBuilder } from '../eb/arc4-bare-method-decorator-builder'
import {
AddressClassBuilder,
Expand Down Expand Up @@ -122,6 +123,7 @@ import {
applicationItxnType,
applicationPType,
ApplicationTxnFunction,
arc28EmitFunction,
arc4AbiMethodDecorator,
arc4BareMethodDecorator,
ArrayPType,
Expand Down Expand Up @@ -236,6 +238,7 @@ export function registerPTypes(typeRegistry: TypeRegistry) {
typeRegistry.register({ ptype: urangeFunction, singletonEb: UrangeFunctionBuilder })
typeRegistry.register({ ptype: TemplateVarFunction, singletonEb: TemplateVarFunctionBuilder })
typeRegistry.register({ ptype: compileFunctionType, singletonEb: CompileFunctionBuilder })
typeRegistry.register({ ptype: arc28EmitFunction, singletonEb: Arc28EmitFunctionBuilder })

typeRegistry.register({ ptype: ContractClassPType, singletonEb: ContractClassBuilder })
typeRegistry.register({ ptype: LogicSigPType, singletonEb: LogicSigClassBuilder })
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const Constants = {
gtxnModuleName: `${algoTsPackage}/gtxn.d.ts`,
itxnModuleName: `${algoTsPackage}/itxn.d.ts`,
compiledModuleName: `${algoTsPackage}/compiled.d.ts`,
arc28ModuleName: `${algoTsPackage}/arc-28.d.ts`,
primitivesModuleName: `${algoTsPackage}/primitives.d.ts`,
arc4EncodedTypesModuleName: `${algoTsPackage}/arc4/encoded-types.d.ts`,
arc4BareDecoratorName: 'arc4.baremethod',
Expand Down
31 changes: 31 additions & 0 deletions tests/approvals/arc28-events.algo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { uint64 } from '@algorandfoundation/algorand-typescript'
import { Contract, emit } from '@algorandfoundation/algorand-typescript'
import { Struct, UintN64 } from '@algorandfoundation/algorand-typescript/arc4'

type Swapped = {
a: uint64
b: uint64
}

class SwappedArc4 extends Struct<{ a: UintN64; b: UintN64 }> {}

class EventEmitter extends Contract {
emitSwapped(a: uint64, b: uint64) {
emit<Swapped>({ a: b, b: a })

const x: Swapped = { a: b, b: a }
emit(x)

const y = new SwappedArc4({
a: new UintN64(b),
b: new UintN64(a),
})
emit(y)

emit('Swapped', b, a)
}

emitCustom(arg0: string, arg1: boolean) {
emit('Custom', arg0, arg1)
}
}
Loading

0 comments on commit d0bfed7

Please sign in to comment.