Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading