Skip to content

Commit

Permalink
feat: implement stub for emit function
Browse files Browse the repository at this point in the history
  • Loading branch information
boblat committed Dec 23, 2024
1 parent a94df7c commit 79838c7
Show file tree
Hide file tree
Showing 11 changed files with 2,743 additions and 1,908 deletions.
2 changes: 1 addition & 1 deletion src/abi-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const getArc4TypeName = (t: TypeInfo): string => {
AssetTransferTxn: 'axfer',
AssetFreezeTxn: 'afrz',
ApplicationTxn: 'appl',
'Tuple<.*>': (t) =>
'Tuple(<.*>)?': (t) =>
`(${Object.values(t.genericArgs as Record<string, TypeInfo>)
.map(getArc4TypeName)
.join(',')})`,
Expand Down
28 changes: 28 additions & 0 deletions src/impl/emit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { internal } from '@algorandfoundation/algorand-typescript'
import { lazyContext } from '../context-helpers/internal-context'
import { DeliberateAny } from '../typescript-helpers'
import { sha512_256 } from './crypto'
import { getArc4Encoded, getArc4TypeName } from './encoded-types'

export function emitImpl<T>(typeInfoString: string, event: T | string, ...eventProps: unknown[]) {
let eventData
let eventName
if (typeof event === 'string') {
eventData = getArc4Encoded(eventProps)
eventName = event
const argTypes = getArc4TypeName((eventData as DeliberateAny).typeInfo)!
if (eventName.indexOf('(') === -1) {
eventName += argTypes
} else if (event.indexOf(argTypes) === -1) {
throw internal.errors.codeError(`Event signature ${event} does not match arg types ${argTypes}`)
}
} else {
eventData = getArc4Encoded(event)
const typeInfo = JSON.parse(typeInfoString)
const argTypes = getArc4TypeName((eventData as DeliberateAny).typeInfo)!
eventName = typeInfo.name.replace(/.*</, '').replace(/>.*/, '') + argTypes
}

const eventHash = sha512_256(eventName)
lazyContext.value.log(eventHash.slice(0, 4).concat(eventData.bytes))
}
36 changes: 18 additions & 18 deletions src/impl/encoded-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,12 @@ export class UFixedNxMImpl<N extends BitSize, M extends number> extends UFixedNx
}

export class ByteImpl extends Byte {
typeInfo: TypeInfo
private value: UintNImpl<8>

constructor(
public typeInfo: TypeInfo | string,
v?: CompatForArc4Int<8>,
) {
constructor(typeInfo: TypeInfo | string, v?: CompatForArc4Int<8>) {
super(v)
this.typeInfo = typeof typeInfo === 'string' ? JSON.parse(typeInfo) : typeInfo
this.value = new UintNImpl<8>(typeInfo, v)
}

Expand Down Expand Up @@ -209,15 +208,14 @@ export class ByteImpl extends Byte {
}

export class StrImpl extends Str {
typeInfo: TypeInfo
private value: Uint8Array

constructor(
public typeInfo: TypeInfo | string,
s?: StringCompat,
) {
constructor(typeInfo: TypeInfo | string, s?: StringCompat) {
super()
const bytesValue = asBytesCls(s ?? '')
const bytesLength = encodeLength(bytesValue.length.asNumber())
this.typeInfo = typeof typeInfo === 'string' ? JSON.parse(typeInfo) : typeInfo
this.value = asUint8Array(bytesLength.concat(bytesValue))
}
get native(): string {
Expand Down Expand Up @@ -255,12 +253,11 @@ const TRUE_BIGINT_VALUE = 128n
const FALSE_BIGINT_VALUE = 0n
export class BoolImpl extends Bool {
private value: Uint8Array
typeInfo: TypeInfo

constructor(
public typeInfo: TypeInfo | string,
v?: boolean,
) {
constructor(typeInfo: TypeInfo | string, v?: boolean) {
super(v)
this.typeInfo = typeof typeInfo === 'string' ? JSON.parse(typeInfo) : typeInfo
this.value = encodingUtil.bigIntToUint8Array(v ? TRUE_BIGINT_VALUE : FALSE_BIGINT_VALUE, 1)
}

Expand Down Expand Up @@ -1154,8 +1151,8 @@ export const arc4Encoders: Record<string, fromBytes<DeliberateAny>> = {
'UFixedNxM<.*>': UFixedNxMImpl.fromBytesImpl,
'StaticArray<.*>': StaticArrayImpl.fromBytesImpl,
'DynamicArray<.*>': DynamicArrayImpl.fromBytesImpl,
Tuple: TupleImpl.fromBytesImpl,
Struct: StructImpl.fromBytesImpl,
'Tuple(<.*>)?': TupleImpl.fromBytesImpl,
'Struct(<.*>)?': StructImpl.fromBytesImpl,
DynamicBytes: DynamicBytesImpl.fromBytesImpl,
'StaticBytes<.*>': StaticBytesImpl.fromBytesImpl,
}
Expand All @@ -1177,8 +1174,8 @@ export const getArc4TypeName = (typeInfo: TypeInfo): string | undefined => {
'UFixedNxM<.*>': UFixedNxMImpl.getArc4TypeName,
'StaticArray<.*>': StaticArrayImpl.getArc4TypeName,
'DynamicArray<.*>': DynamicArrayImpl.getArc4TypeName,
Tuple: TupleImpl.getArc4TypeName,
Struct: StructImpl.getArc4TypeName,
'Tuple(<.*>)?': TupleImpl.getArc4TypeName,
'Struct(<.*>)?': StructImpl.getArc4TypeName,
DynamicBytes: DynamicBytesImpl.getArc4TypeName,
'StaticBytes<.*>': StaticBytesImpl.getArc4TypeName,
}
Expand Down Expand Up @@ -1213,7 +1210,7 @@ const getNativeValue = (value: DeliberateAny): DeliberateAny => {
return native
}

const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
export const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
if (value instanceof ARC4Encoded) {
return value
}
Expand Down Expand Up @@ -1263,7 +1260,10 @@ const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
return acc.concat(getArc4Encoded(cur))
}, [])
const genericArgs: TypeInfo[] = result.map((x) => (x as DeliberateAny).typeInfo)
const typeInfo = { name: 'Struct', genericArgs: Object.fromEntries(Object.keys(value).map((x, i) => [x, genericArgs[i]])) }
const typeInfo = {
name: `Struct<${value.constructor.name}>`,
genericArgs: Object.fromEntries(Object.keys(value).map((x, i) => [x, genericArgs[i]])),
}
return new StructImpl(typeInfo, Object.fromEntries(Object.keys(value).map((x, i) => [x, result[i]])))
}

Expand Down
1 change: 1 addition & 0 deletions src/runtime-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DeliberateAny } from './typescript-helpers'
import { nameOfType } from './util'

export { attachAbiMetadata } from './abi-metadata'
export { emitImpl } from './impl/emit'
export * from './impl/encoded-types'
export { decodeArc4Impl, encodeArc4Impl } from './impl/encoded-types'
export { ensureBudgetImpl } from './impl/ensure-budget'
Expand Down
12 changes: 10 additions & 2 deletions src/test-transformer/visitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const algotsModulePaths = [
type VisitorHelper = {
additionalStatements: ts.Statement[]
resolveType(node: ts.Node): ptypes.PType
resolveTypeParameters(node: ts.CallExpression): ptypes.PType[]
sourceLocation(node: ts.Node): SourceLocation
tryGetSymbol(node: ts.Node): ts.Symbol | undefined
}
Expand All @@ -44,6 +45,9 @@ export class SourceFileVisitor {
return ptypes.anyPType
}
},
resolveTypeParameters(node: ts.CallExpression) {
return typeResolver.resolveTypeParameters(node, this.sourceLocation(node))
},
tryGetSymbol(node: ts.Node): ts.Symbol | undefined {
const s = typeChecker.getSymbolAtLocation(node)
return s && s.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(s) : s
Expand Down Expand Up @@ -112,6 +116,9 @@ class ExpressionVisitor {
if (ts.isCallExpression(updatedNode)) {
const stubbedFunctionName = tryGetStubbedFunctionName(updatedNode, this.helper)
const infos = [info]
if (isCallingEmit(stubbedFunctionName)) {
infos[0] = this.helper.resolveTypeParameters(updatedNode).map(getGenericTypeInfo)[0]
}
if (isCallingDecodeArc4(stubbedFunctionName)) {
const targetType = ptypes.ptypeToArc4EncodedType(type, this.helper.sourceLocation(node))
const targetTypeInfo = getGenericTypeInfo(targetType)
Expand Down Expand Up @@ -332,7 +339,7 @@ const getGenericTypeInfo = (type: ptypes.PType): TypeInfo => {
} else if (type instanceof ptypes.UintNType) {
genericArgs.push({ name: type.n.toString() })
} else if (type instanceof ptypes.ARC4StructType) {
typeName = 'Struct'
typeName = `Struct<${type.name}>`
genericArgs = Object.fromEntries(
Object.entries(type.fields)
.map(([key, value]) => [key, getGenericTypeInfo(value)])
Expand Down Expand Up @@ -360,8 +367,9 @@ const tryGetStubbedFunctionName = (node: ts.CallExpression, helper: VisitorHelpe
if (sourceFileName && !algotsModulePaths.some((s) => sourceFileName.includes(s))) return undefined
}
const functionName = functionSymbol?.getName() ?? identityExpression.text
const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'TemplateVar', 'ensureBudget']
const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'TemplateVar', 'ensureBudget', 'emit']
return stubbedFunctionNames.includes(functionName) ? functionName : undefined
}

const isCallingDecodeArc4 = (functionName: string | undefined): boolean => ['decodeArc4', 'encodeArc4'].includes(functionName ?? '')
const isCallingEmit = (functionName: string | undefined): boolean => 'emit' === (functionName ?? '')
138 changes: 138 additions & 0 deletions tests/arc4/emit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { AppSpec } from '@algorandfoundation/algokit-utils/types/app-spec'
import { arc4, BigUint, biguint, Bytes, bytes, emit, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
import { afterEach, describe, expect, it } from 'vitest'
import { MAX_UINT512, MAX_UINT64 } from '../../src/constants'
import appSpecJson from '../artifacts/arc4-primitive-ops/data/Arc4PrimitiveOpsContract.arc32.json'
import { getAlgorandAppClient, getAvmResultLog } from '../avm-invoker'

import { asBigUintCls, asNumber, asUint8Array } from '../../src/util'

class Swapped {
a: string
b: biguint
c: uint64
d: bytes
e: uint64
f: boolean
g: bytes
h: string

constructor(a: string, b: biguint, c: uint64, d: bytes, e: uint64, f: boolean, g: bytes, h: string) {
this.a = a
this.b = b
this.c = c
this.d = d
this.e = e
this.f = f
this.g = g
this.h = h
}
}
class SwappedArc4 extends arc4.Struct<{
m: arc4.UintN<64>
n: arc4.UintN<256>
o: arc4.UFixedNxM<32, 8>
p: arc4.UFixedNxM<256, 16>
q: arc4.Bool
r: arc4.StaticArray<arc4.UintN8, 3>
s: arc4.DynamicArray<arc4.UintN16>
t: arc4.Tuple<[arc4.UintN32, arc4.UintN64, arc4.Str]>
}> {}

describe('arc4.emit', async () => {
const appClient = await getAlgorandAppClient(appSpecJson as AppSpec)
const ctx = new TestExecutionContext()

afterEach(async () => {
ctx.reset()
})

it('should emit the correct values', async () => {
const test_data = new Swapped('hello', BigUint(MAX_UINT512), Uint64(MAX_UINT64), Bytes('world'), 16, false, Bytes('test'), 'greetings')

const test_data_arc4 = new SwappedArc4({
m: new arc4.UintN64(42),
n: new arc4.UintN256(512),
o: new arc4.UFixedNxM<32, 8>('42.94967295'),
p: new arc4.UFixedNxM<256, 16>('25.5'),
q: new arc4.Bool(true),
r: new arc4.StaticArray(new arc4.UintN8(1), new arc4.UintN8(2), new arc4.UintN8(3)),
s: new arc4.DynamicArray(new arc4.UintN16(1), new arc4.UintN16(2), new arc4.UintN16(3)),
t: new arc4.Tuple(new arc4.UintN32(1), new arc4.UintN64(2), new arc4.Str('hello')),
})
const avm_result = await getAvmResultLog(
{ appClient },
'verify_emit',
test_data.a,
test_data.b.valueOf(),
test_data.c.valueOf(),
asUint8Array(test_data.d),
test_data.e,
test_data.f,
asUint8Array(test_data.g),
test_data.h,
test_data_arc4.m.native.valueOf(),
test_data_arc4.n.native.valueOf(),
asBigUintCls(test_data_arc4.o.bytes).asBigInt(),
asBigUintCls(test_data_arc4.p.bytes).asBigInt(),
test_data_arc4.q.native,
asUint8Array(test_data_arc4.r.bytes),
asUint8Array(test_data_arc4.s.bytes),
asUint8Array(test_data_arc4.t.bytes),
)

expect(avm_result).toBeInstanceOf(Array)
const avmLogs = avm_result?.map(Bytes)

const dummy_app = ctx.any.application()
const app_txn = ctx.any.txn.applicationCall({ appId: dummy_app })
ctx.txn.createScope([app_txn]).execute(() => {
emit(test_data_arc4)
emit(
'Swapped',
test_data.a,
test_data.b,
test_data.c,
test_data.d,
test_data.e,
test_data.f,
test_data.g,
test_data.h,
test_data_arc4.m,
test_data_arc4.n,
test_data_arc4.o,
test_data_arc4.p,
test_data_arc4.q,
test_data_arc4.r,
test_data_arc4.s,
test_data_arc4.t,
)
emit(
'Swapped(string,uint512,uint64,byte[],uint64,bool,byte[],string,uint64,uint256,ufixed32x8,ufixed256x16,bool,uint8[3],uint16[],(uint32,uint64,string))',
test_data.a,
test_data.b,
test_data.c,
test_data.d,
test_data.e,
test_data.f,
test_data.g,
test_data.h,
test_data_arc4.m,
test_data_arc4.n,
test_data_arc4.o,
test_data_arc4.p,
test_data_arc4.q,
test_data_arc4.r,
test_data_arc4.s,
test_data_arc4.t,
)
const arc4_result = [...Array(asNumber(app_txn.numLogs)).keys()].fill(0).map((_, i) => app_txn.logs(i))

expect(arc4_result[0]).toEqual(avmLogs![0])
expect(arc4_result[1]).toEqual(avmLogs![1])
expect(arc4_result[1]).toEqual(arc4_result[2])
expect(arc4_result[2]).toEqual(avmLogs![2])
})
})
})
60 changes: 59 additions & 1 deletion tests/artifacts/arc4-primitive-ops/contract.algo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { arc4, BigUint, bytes } from '@algorandfoundation/algorand-typescript'
import { arc4, BigUint, bytes, emit } from '@algorandfoundation/algorand-typescript'
import { Bool, Byte, Contract, interpretAsArc4, Str, UFixedNxM, UintN } from '@algorandfoundation/algorand-typescript/arc4'

export class Arc4PrimitiveOpsContract extends Contract {
Expand Down Expand Up @@ -344,4 +344,62 @@ export class Arc4PrimitiveOpsContract extends Contract {
public verify_bool_from_log(a: bytes): Bool {
return interpretAsArc4<Bool>(a, 'log')
}

// TODO: recompile when puya-ts is updated
@arc4.abimethod()
public verify_emit(
a: arc4.Str,
b: arc4.UintN<512>,
c: arc4.UintN64,
d: arc4.DynamicBytes,
e: arc4.UintN64,
f: arc4.Bool,
g: arc4.DynamicBytes,
h: arc4.Str,
m: arc4.UintN<64>,
n: arc4.UintN<256>,
o: arc4.UFixedNxM<32, 8>,
p: arc4.UFixedNxM<256, 16>,
q: arc4.Bool,
r: bytes,
s: bytes,
t: bytes,
): void {
const arc4_r = interpretAsArc4<arc4.StaticArray<arc4.UintN8, 3>>(r)
const arc4_s = interpretAsArc4<arc4.DynamicArray<arc4.UintN16>>(s)
const arc4_t = interpretAsArc4<arc4.Tuple<[arc4.UintN32, arc4.UintN64, arc4.Str]>>(t)

emit(new SwappedArc4({ m, n, o, p, q, r: arc4_r, s: arc4_s, t: arc4_t }))
emit('Swapped', a, b, c, d, e, f, g, h, m, n, o, p, q, arc4_r.copy(), arc4_s.copy(), arc4_t)
emit(
'Swapped(string,uint512,uint64,byte[],uint64,bool,byte[],string,uint64,uint256,ufixed32x8,ufixed256x16,bool,uint8[3],uint16[],(uint32,uint64,string))',
a,
b,
c,
d,
e,
f,
g,
h,
m,
n,
o,
p,
q,
arc4_r.copy(),
arc4_s.copy(),
arc4_t,
)
}
}

class SwappedArc4 extends arc4.Struct<{
m: arc4.UintN<64>
n: arc4.UintN<256>
o: arc4.UFixedNxM<32, 8>
p: arc4.UFixedNxM<256, 16>
q: arc4.Bool
r: arc4.StaticArray<arc4.UintN8, 3>
s: arc4.DynamicArray<arc4.UintN16>
t: arc4.Tuple<[arc4.UintN32, arc4.UintN64, arc4.Str]>
}> {}
Loading

0 comments on commit 79838c7

Please sign in to comment.