Skip to content

Commit 79838c7

Browse files
committed
feat: implement stub for emit function
1 parent a94df7c commit 79838c7

File tree

11 files changed

+2743
-1908
lines changed

11 files changed

+2743
-1908
lines changed

src/abi-metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ const getArc4TypeName = (t: TypeInfo): string => {
103103
AssetTransferTxn: 'axfer',
104104
AssetFreezeTxn: 'afrz',
105105
ApplicationTxn: 'appl',
106-
'Tuple<.*>': (t) =>
106+
'Tuple(<.*>)?': (t) =>
107107
`(${Object.values(t.genericArgs as Record<string, TypeInfo>)
108108
.map(getArc4TypeName)
109109
.join(',')})`,

src/impl/emit.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { internal } from '@algorandfoundation/algorand-typescript'
2+
import { lazyContext } from '../context-helpers/internal-context'
3+
import { DeliberateAny } from '../typescript-helpers'
4+
import { sha512_256 } from './crypto'
5+
import { getArc4Encoded, getArc4TypeName } from './encoded-types'
6+
7+
export function emitImpl<T>(typeInfoString: string, event: T | string, ...eventProps: unknown[]) {
8+
let eventData
9+
let eventName
10+
if (typeof event === 'string') {
11+
eventData = getArc4Encoded(eventProps)
12+
eventName = event
13+
const argTypes = getArc4TypeName((eventData as DeliberateAny).typeInfo)!
14+
if (eventName.indexOf('(') === -1) {
15+
eventName += argTypes
16+
} else if (event.indexOf(argTypes) === -1) {
17+
throw internal.errors.codeError(`Event signature ${event} does not match arg types ${argTypes}`)
18+
}
19+
} else {
20+
eventData = getArc4Encoded(event)
21+
const typeInfo = JSON.parse(typeInfoString)
22+
const argTypes = getArc4TypeName((eventData as DeliberateAny).typeInfo)!
23+
eventName = typeInfo.name.replace(/.*</, '').replace(/>.*/, '') + argTypes
24+
}
25+
26+
const eventHash = sha512_256(eventName)
27+
lazyContext.value.log(eventHash.slice(0, 4).concat(eventData.bytes))
28+
}

src/impl/encoded-types.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,12 @@ export class UFixedNxMImpl<N extends BitSize, M extends number> extends UFixedNx
167167
}
168168

169169
export class ByteImpl extends Byte {
170+
typeInfo: TypeInfo
170171
private value: UintNImpl<8>
171172

172-
constructor(
173-
public typeInfo: TypeInfo | string,
174-
v?: CompatForArc4Int<8>,
175-
) {
173+
constructor(typeInfo: TypeInfo | string, v?: CompatForArc4Int<8>) {
176174
super(v)
175+
this.typeInfo = typeof typeInfo === 'string' ? JSON.parse(typeInfo) : typeInfo
177176
this.value = new UintNImpl<8>(typeInfo, v)
178177
}
179178

@@ -209,15 +208,14 @@ export class ByteImpl extends Byte {
209208
}
210209

211210
export class StrImpl extends Str {
211+
typeInfo: TypeInfo
212212
private value: Uint8Array
213213

214-
constructor(
215-
public typeInfo: TypeInfo | string,
216-
s?: StringCompat,
217-
) {
214+
constructor(typeInfo: TypeInfo | string, s?: StringCompat) {
218215
super()
219216
const bytesValue = asBytesCls(s ?? '')
220217
const bytesLength = encodeLength(bytesValue.length.asNumber())
218+
this.typeInfo = typeof typeInfo === 'string' ? JSON.parse(typeInfo) : typeInfo
221219
this.value = asUint8Array(bytesLength.concat(bytesValue))
222220
}
223221
get native(): string {
@@ -255,12 +253,11 @@ const TRUE_BIGINT_VALUE = 128n
255253
const FALSE_BIGINT_VALUE = 0n
256254
export class BoolImpl extends Bool {
257255
private value: Uint8Array
256+
typeInfo: TypeInfo
258257

259-
constructor(
260-
public typeInfo: TypeInfo | string,
261-
v?: boolean,
262-
) {
258+
constructor(typeInfo: TypeInfo | string, v?: boolean) {
263259
super(v)
260+
this.typeInfo = typeof typeInfo === 'string' ? JSON.parse(typeInfo) : typeInfo
264261
this.value = encodingUtil.bigIntToUint8Array(v ? TRUE_BIGINT_VALUE : FALSE_BIGINT_VALUE, 1)
265262
}
266263

@@ -1154,8 +1151,8 @@ export const arc4Encoders: Record<string, fromBytes<DeliberateAny>> = {
11541151
'UFixedNxM<.*>': UFixedNxMImpl.fromBytesImpl,
11551152
'StaticArray<.*>': StaticArrayImpl.fromBytesImpl,
11561153
'DynamicArray<.*>': DynamicArrayImpl.fromBytesImpl,
1157-
Tuple: TupleImpl.fromBytesImpl,
1158-
Struct: StructImpl.fromBytesImpl,
1154+
'Tuple(<.*>)?': TupleImpl.fromBytesImpl,
1155+
'Struct(<.*>)?': StructImpl.fromBytesImpl,
11591156
DynamicBytes: DynamicBytesImpl.fromBytesImpl,
11601157
'StaticBytes<.*>': StaticBytesImpl.fromBytesImpl,
11611158
}
@@ -1177,8 +1174,8 @@ export const getArc4TypeName = (typeInfo: TypeInfo): string | undefined => {
11771174
'UFixedNxM<.*>': UFixedNxMImpl.getArc4TypeName,
11781175
'StaticArray<.*>': StaticArrayImpl.getArc4TypeName,
11791176
'DynamicArray<.*>': DynamicArrayImpl.getArc4TypeName,
1180-
Tuple: TupleImpl.getArc4TypeName,
1181-
Struct: StructImpl.getArc4TypeName,
1177+
'Tuple(<.*>)?': TupleImpl.getArc4TypeName,
1178+
'Struct(<.*>)?': StructImpl.getArc4TypeName,
11821179
DynamicBytes: DynamicBytesImpl.getArc4TypeName,
11831180
'StaticBytes<.*>': StaticBytesImpl.getArc4TypeName,
11841181
}
@@ -1213,7 +1210,7 @@ const getNativeValue = (value: DeliberateAny): DeliberateAny => {
12131210
return native
12141211
}
12151212

1216-
const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
1213+
export const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
12171214
if (value instanceof ARC4Encoded) {
12181215
return value
12191216
}
@@ -1263,7 +1260,10 @@ const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
12631260
return acc.concat(getArc4Encoded(cur))
12641261
}, [])
12651262
const genericArgs: TypeInfo[] = result.map((x) => (x as DeliberateAny).typeInfo)
1266-
const typeInfo = { name: 'Struct', genericArgs: Object.fromEntries(Object.keys(value).map((x, i) => [x, genericArgs[i]])) }
1263+
const typeInfo = {
1264+
name: `Struct<${value.constructor.name}>`,
1265+
genericArgs: Object.fromEntries(Object.keys(value).map((x, i) => [x, genericArgs[i]])),
1266+
}
12671267
return new StructImpl(typeInfo, Object.fromEntries(Object.keys(value).map((x, i) => [x, result[i]])))
12681268
}
12691269

src/runtime-helpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DeliberateAny } from './typescript-helpers'
77
import { nameOfType } from './util'
88

99
export { attachAbiMetadata } from './abi-metadata'
10+
export { emitImpl } from './impl/emit'
1011
export * from './impl/encoded-types'
1112
export { decodeArc4Impl, encodeArc4Impl } from './impl/encoded-types'
1213
export { ensureBudgetImpl } from './impl/ensure-budget'

src/test-transformer/visitors.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const algotsModulePaths = [
2121
type VisitorHelper = {
2222
additionalStatements: ts.Statement[]
2323
resolveType(node: ts.Node): ptypes.PType
24+
resolveTypeParameters(node: ts.CallExpression): ptypes.PType[]
2425
sourceLocation(node: ts.Node): SourceLocation
2526
tryGetSymbol(node: ts.Node): ts.Symbol | undefined
2627
}
@@ -44,6 +45,9 @@ export class SourceFileVisitor {
4445
return ptypes.anyPType
4546
}
4647
},
48+
resolveTypeParameters(node: ts.CallExpression) {
49+
return typeResolver.resolveTypeParameters(node, this.sourceLocation(node))
50+
},
4751
tryGetSymbol(node: ts.Node): ts.Symbol | undefined {
4852
const s = typeChecker.getSymbolAtLocation(node)
4953
return s && s.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(s) : s
@@ -112,6 +116,9 @@ class ExpressionVisitor {
112116
if (ts.isCallExpression(updatedNode)) {
113117
const stubbedFunctionName = tryGetStubbedFunctionName(updatedNode, this.helper)
114118
const infos = [info]
119+
if (isCallingEmit(stubbedFunctionName)) {
120+
infos[0] = this.helper.resolveTypeParameters(updatedNode).map(getGenericTypeInfo)[0]
121+
}
115122
if (isCallingDecodeArc4(stubbedFunctionName)) {
116123
const targetType = ptypes.ptypeToArc4EncodedType(type, this.helper.sourceLocation(node))
117124
const targetTypeInfo = getGenericTypeInfo(targetType)
@@ -332,7 +339,7 @@ const getGenericTypeInfo = (type: ptypes.PType): TypeInfo => {
332339
} else if (type instanceof ptypes.UintNType) {
333340
genericArgs.push({ name: type.n.toString() })
334341
} else if (type instanceof ptypes.ARC4StructType) {
335-
typeName = 'Struct'
342+
typeName = `Struct<${type.name}>`
336343
genericArgs = Object.fromEntries(
337344
Object.entries(type.fields)
338345
.map(([key, value]) => [key, getGenericTypeInfo(value)])
@@ -360,8 +367,9 @@ const tryGetStubbedFunctionName = (node: ts.CallExpression, helper: VisitorHelpe
360367
if (sourceFileName && !algotsModulePaths.some((s) => sourceFileName.includes(s))) return undefined
361368
}
362369
const functionName = functionSymbol?.getName() ?? identityExpression.text
363-
const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'TemplateVar', 'ensureBudget']
370+
const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'TemplateVar', 'ensureBudget', 'emit']
364371
return stubbedFunctionNames.includes(functionName) ? functionName : undefined
365372
}
366373

367374
const isCallingDecodeArc4 = (functionName: string | undefined): boolean => ['decodeArc4', 'encodeArc4'].includes(functionName ?? '')
375+
const isCallingEmit = (functionName: string | undefined): boolean => 'emit' === (functionName ?? '')

tests/arc4/emit.spec.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { AppSpec } from '@algorandfoundation/algokit-utils/types/app-spec'
2+
import { arc4, BigUint, biguint, Bytes, bytes, emit, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
3+
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
4+
import { afterEach, describe, expect, it } from 'vitest'
5+
import { MAX_UINT512, MAX_UINT64 } from '../../src/constants'
6+
import appSpecJson from '../artifacts/arc4-primitive-ops/data/Arc4PrimitiveOpsContract.arc32.json'
7+
import { getAlgorandAppClient, getAvmResultLog } from '../avm-invoker'
8+
9+
import { asBigUintCls, asNumber, asUint8Array } from '../../src/util'
10+
11+
class Swapped {
12+
a: string
13+
b: biguint
14+
c: uint64
15+
d: bytes
16+
e: uint64
17+
f: boolean
18+
g: bytes
19+
h: string
20+
21+
constructor(a: string, b: biguint, c: uint64, d: bytes, e: uint64, f: boolean, g: bytes, h: string) {
22+
this.a = a
23+
this.b = b
24+
this.c = c
25+
this.d = d
26+
this.e = e
27+
this.f = f
28+
this.g = g
29+
this.h = h
30+
}
31+
}
32+
class SwappedArc4 extends arc4.Struct<{
33+
m: arc4.UintN<64>
34+
n: arc4.UintN<256>
35+
o: arc4.UFixedNxM<32, 8>
36+
p: arc4.UFixedNxM<256, 16>
37+
q: arc4.Bool
38+
r: arc4.StaticArray<arc4.UintN8, 3>
39+
s: arc4.DynamicArray<arc4.UintN16>
40+
t: arc4.Tuple<[arc4.UintN32, arc4.UintN64, arc4.Str]>
41+
}> {}
42+
43+
describe('arc4.emit', async () => {
44+
const appClient = await getAlgorandAppClient(appSpecJson as AppSpec)
45+
const ctx = new TestExecutionContext()
46+
47+
afterEach(async () => {
48+
ctx.reset()
49+
})
50+
51+
it('should emit the correct values', async () => {
52+
const test_data = new Swapped('hello', BigUint(MAX_UINT512), Uint64(MAX_UINT64), Bytes('world'), 16, false, Bytes('test'), 'greetings')
53+
54+
const test_data_arc4 = new SwappedArc4({
55+
m: new arc4.UintN64(42),
56+
n: new arc4.UintN256(512),
57+
o: new arc4.UFixedNxM<32, 8>('42.94967295'),
58+
p: new arc4.UFixedNxM<256, 16>('25.5'),
59+
q: new arc4.Bool(true),
60+
r: new arc4.StaticArray(new arc4.UintN8(1), new arc4.UintN8(2), new arc4.UintN8(3)),
61+
s: new arc4.DynamicArray(new arc4.UintN16(1), new arc4.UintN16(2), new arc4.UintN16(3)),
62+
t: new arc4.Tuple(new arc4.UintN32(1), new arc4.UintN64(2), new arc4.Str('hello')),
63+
})
64+
const avm_result = await getAvmResultLog(
65+
{ appClient },
66+
'verify_emit',
67+
test_data.a,
68+
test_data.b.valueOf(),
69+
test_data.c.valueOf(),
70+
asUint8Array(test_data.d),
71+
test_data.e,
72+
test_data.f,
73+
asUint8Array(test_data.g),
74+
test_data.h,
75+
test_data_arc4.m.native.valueOf(),
76+
test_data_arc4.n.native.valueOf(),
77+
asBigUintCls(test_data_arc4.o.bytes).asBigInt(),
78+
asBigUintCls(test_data_arc4.p.bytes).asBigInt(),
79+
test_data_arc4.q.native,
80+
asUint8Array(test_data_arc4.r.bytes),
81+
asUint8Array(test_data_arc4.s.bytes),
82+
asUint8Array(test_data_arc4.t.bytes),
83+
)
84+
85+
expect(avm_result).toBeInstanceOf(Array)
86+
const avmLogs = avm_result?.map(Bytes)
87+
88+
const dummy_app = ctx.any.application()
89+
const app_txn = ctx.any.txn.applicationCall({ appId: dummy_app })
90+
ctx.txn.createScope([app_txn]).execute(() => {
91+
emit(test_data_arc4)
92+
emit(
93+
'Swapped',
94+
test_data.a,
95+
test_data.b,
96+
test_data.c,
97+
test_data.d,
98+
test_data.e,
99+
test_data.f,
100+
test_data.g,
101+
test_data.h,
102+
test_data_arc4.m,
103+
test_data_arc4.n,
104+
test_data_arc4.o,
105+
test_data_arc4.p,
106+
test_data_arc4.q,
107+
test_data_arc4.r,
108+
test_data_arc4.s,
109+
test_data_arc4.t,
110+
)
111+
emit(
112+
'Swapped(string,uint512,uint64,byte[],uint64,bool,byte[],string,uint64,uint256,ufixed32x8,ufixed256x16,bool,uint8[3],uint16[],(uint32,uint64,string))',
113+
test_data.a,
114+
test_data.b,
115+
test_data.c,
116+
test_data.d,
117+
test_data.e,
118+
test_data.f,
119+
test_data.g,
120+
test_data.h,
121+
test_data_arc4.m,
122+
test_data_arc4.n,
123+
test_data_arc4.o,
124+
test_data_arc4.p,
125+
test_data_arc4.q,
126+
test_data_arc4.r,
127+
test_data_arc4.s,
128+
test_data_arc4.t,
129+
)
130+
const arc4_result = [...Array(asNumber(app_txn.numLogs)).keys()].fill(0).map((_, i) => app_txn.logs(i))
131+
132+
expect(arc4_result[0]).toEqual(avmLogs![0])
133+
expect(arc4_result[1]).toEqual(avmLogs![1])
134+
expect(arc4_result[1]).toEqual(arc4_result[2])
135+
expect(arc4_result[2]).toEqual(avmLogs![2])
136+
})
137+
})
138+
})

tests/artifacts/arc4-primitive-ops/contract.algo.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { arc4, BigUint, bytes } from '@algorandfoundation/algorand-typescript'
1+
import { arc4, BigUint, bytes, emit } from '@algorandfoundation/algorand-typescript'
22
import { Bool, Byte, Contract, interpretAsArc4, Str, UFixedNxM, UintN } from '@algorandfoundation/algorand-typescript/arc4'
33

44
export class Arc4PrimitiveOpsContract extends Contract {
@@ -344,4 +344,62 @@ export class Arc4PrimitiveOpsContract extends Contract {
344344
public verify_bool_from_log(a: bytes): Bool {
345345
return interpretAsArc4<Bool>(a, 'log')
346346
}
347+
348+
// TODO: recompile when puya-ts is updated
349+
@arc4.abimethod()
350+
public verify_emit(
351+
a: arc4.Str,
352+
b: arc4.UintN<512>,
353+
c: arc4.UintN64,
354+
d: arc4.DynamicBytes,
355+
e: arc4.UintN64,
356+
f: arc4.Bool,
357+
g: arc4.DynamicBytes,
358+
h: arc4.Str,
359+
m: arc4.UintN<64>,
360+
n: arc4.UintN<256>,
361+
o: arc4.UFixedNxM<32, 8>,
362+
p: arc4.UFixedNxM<256, 16>,
363+
q: arc4.Bool,
364+
r: bytes,
365+
s: bytes,
366+
t: bytes,
367+
): void {
368+
const arc4_r = interpretAsArc4<arc4.StaticArray<arc4.UintN8, 3>>(r)
369+
const arc4_s = interpretAsArc4<arc4.DynamicArray<arc4.UintN16>>(s)
370+
const arc4_t = interpretAsArc4<arc4.Tuple<[arc4.UintN32, arc4.UintN64, arc4.Str]>>(t)
371+
372+
emit(new SwappedArc4({ m, n, o, p, q, r: arc4_r, s: arc4_s, t: arc4_t }))
373+
emit('Swapped', a, b, c, d, e, f, g, h, m, n, o, p, q, arc4_r.copy(), arc4_s.copy(), arc4_t)
374+
emit(
375+
'Swapped(string,uint512,uint64,byte[],uint64,bool,byte[],string,uint64,uint256,ufixed32x8,ufixed256x16,bool,uint8[3],uint16[],(uint32,uint64,string))',
376+
a,
377+
b,
378+
c,
379+
d,
380+
e,
381+
f,
382+
g,
383+
h,
384+
m,
385+
n,
386+
o,
387+
p,
388+
q,
389+
arc4_r.copy(),
390+
arc4_s.copy(),
391+
arc4_t,
392+
)
393+
}
347394
}
395+
396+
class SwappedArc4 extends arc4.Struct<{
397+
m: arc4.UintN<64>
398+
n: arc4.UintN<256>
399+
o: arc4.UFixedNxM<32, 8>
400+
p: arc4.UFixedNxM<256, 16>
401+
q: arc4.Bool
402+
r: arc4.StaticArray<arc4.UintN8, 3>
403+
s: arc4.DynamicArray<arc4.UintN16>
404+
t: arc4.Tuple<[arc4.UintN32, arc4.UintN64, arc4.Str]>
405+
}> {}

0 commit comments

Comments
 (0)