Skip to content

Commit a94df7c

Browse files
authored
Merge pull request #18 from algorandfoundation/feat-encode-decode-arc4
feat: implement stubs for decodeArc4 and encodeArc4 functions
2 parents bd713dc + 73dd3e7 commit a94df7c

File tree

9 files changed

+393
-146
lines changed

9 files changed

+393
-146
lines changed

package-lock.json

+111-126
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
},
6565
"dependencies": {
6666
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.23",
67-
"@algorandfoundation/puya-ts": "^1.0.0-alpha.35",
67+
"@algorandfoundation/puya-ts": "^1.0.0-alpha.36",
6868
"elliptic": "^6.5.7",
6969
"js-sha256": "^0.11.0",
7070
"js-sha3": "^0.9.3",

src/impl/encoded-types.ts

+120-12
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,20 @@ import {
2424
BITS_IN_BYTE,
2525
UINT64_SIZE,
2626
} from '../constants'
27+
import { lazyContext } from '../context-helpers/internal-context'
2728
import { fromBytes, TypeInfo } from '../encoders'
2829
import { DeliberateAny } from '../typescript-helpers'
29-
import { asBigUint, asBigUintCls, asBytesCls, asUint64, asUint8Array, conactUint8Arrays, uint8ArrayToNumber } from '../util'
30+
import { asBigInt, asBigUint, asBigUintCls, asBytesCls, asUint64, asUint8Array, conactUint8Arrays, uint8ArrayToNumber } from '../util'
31+
import { AccountCls } from './account'
32+
import { ApplicationCls } from './application'
33+
import { AssetCls } from './asset'
34+
import { ApplicationTransaction } from './transactions'
3035

3136
const ABI_LENGTH_SIZE = 2
3237
const maxBigIntValue = (bitSize: number) => 2n ** BigInt(bitSize) - 1n
3338
const maxBytesLength = (bitSize: number) => Math.floor(bitSize / BITS_IN_BYTE)
3439
const encodeLength = (length: number) => new internal.primitives.BytesCls(encodingUtil.bigIntToUint8Array(BigInt(length), ABI_LENGTH_SIZE))
40+
3541
type CompatForArc4Int<N extends BitSize> = N extends 8 | 16 | 32 | 64 ? Uint64Compat : BigUintCompat
3642
export class UintNImpl<N extends BitSize> extends UintN<N> {
3743
private value: Uint8Array
@@ -99,7 +105,7 @@ export class UFixedNxMImpl<N extends BitSize, M extends number> extends UFixedNx
99105
private value: Uint8Array
100106
private bitSize: N
101107
private precision: M
102-
private typeInfo: TypeInfo
108+
typeInfo: TypeInfo
103109

104110
constructor(typeInfo: TypeInfo | string, v: `${number}.${number}`) {
105111
super(v)
@@ -163,7 +169,10 @@ export class UFixedNxMImpl<N extends BitSize, M extends number> extends UFixedNx
163169
export class ByteImpl extends Byte {
164170
private value: UintNImpl<8>
165171

166-
constructor(typeInfo: TypeInfo | string, v?: CompatForArc4Int<8>) {
172+
constructor(
173+
public typeInfo: TypeInfo | string,
174+
v?: CompatForArc4Int<8>,
175+
) {
167176
super(v)
168177
this.value = new UintNImpl<8>(typeInfo, v)
169178
}
@@ -202,7 +211,10 @@ export class ByteImpl extends Byte {
202211
export class StrImpl extends Str {
203212
private value: Uint8Array
204213

205-
constructor(_typeInfo: TypeInfo | string, s?: StringCompat) {
214+
constructor(
215+
public typeInfo: TypeInfo | string,
216+
s?: StringCompat,
217+
) {
206218
super()
207219
const bytesValue = asBytesCls(s ?? '')
208220
const bytesLength = encodeLength(bytesValue.length.asNumber())
@@ -244,7 +256,10 @@ const FALSE_BIGINT_VALUE = 0n
244256
export class BoolImpl extends Bool {
245257
private value: Uint8Array
246258

247-
constructor(_typeInfo: TypeInfo | string, v?: boolean) {
259+
constructor(
260+
public typeInfo: TypeInfo | string,
261+
v?: boolean,
262+
) {
248263
super(v)
249264
this.value = encodingUtil.bigIntToUint8Array(v ? TRUE_BIGINT_VALUE : FALSE_BIGINT_VALUE, 1)
250265
}
@@ -320,8 +335,8 @@ const arrayProxyHandler = <TItem>() => ({
320335
export class StaticArrayImpl<TItem extends ARC4Encoded, TLength extends number> extends StaticArray<TItem, TLength> {
321336
private value?: TItem[]
322337
private uint8ArrayValue?: Uint8Array
323-
private typeInfo: TypeInfo
324338
private size: number
339+
typeInfo: TypeInfo
325340
genericArgs: StaticArrayGenericArgs
326341

327342
constructor(typeInfo: TypeInfo | string, ...items: TItem[] & { length: TLength })
@@ -383,6 +398,10 @@ export class StaticArrayImpl<TItem extends ARC4Encoded, TLength extends number>
383398
return StaticArrayImpl.fromBytesImpl(this.bytes, JSON.stringify(this.typeInfo)) as StaticArrayImpl<TItem, TLength>
384399
}
385400

401+
get native(): TItem[] {
402+
return this.items
403+
}
404+
386405
static fromBytesImpl(
387406
value: internal.primitives.StubBytesCompat | Uint8Array,
388407
typeInfo: string | TypeInfo,
@@ -425,7 +444,7 @@ export class StaticArrayImpl<TItem extends ARC4Encoded, TLength extends number>
425444
}
426445

427446
export class AddressImpl extends Address {
428-
private typeInfo: TypeInfo
447+
typeInfo: TypeInfo
429448
private value: StaticArrayImpl<ByteImpl, 32>
430449

431450
constructor(typeInfo: TypeInfo | string, value?: Account | string | bytes) {
@@ -499,7 +518,7 @@ const readLength = (value: Uint8Array): readonly [number, Uint8Array] => {
499518
export class DynamicArrayImpl<TItem extends ARC4Encoded> extends DynamicArray<TItem> {
500519
private value?: TItem[]
501520
private uint8ArrayValue?: Uint8Array
502-
private typeInfo: TypeInfo
521+
typeInfo: TypeInfo
503522
genericArgs: DynamicArrayGenericArgs
504523

505524
constructor(typeInfo: TypeInfo | string, ...items: TItem[]) {
@@ -554,6 +573,10 @@ export class DynamicArrayImpl<TItem extends ARC4Encoded> extends DynamicArray<TI
554573
return DynamicArrayImpl.fromBytesImpl(this.bytes, JSON.stringify(this.typeInfo)) as DynamicArrayImpl<TItem>
555574
}
556575

576+
get native(): TItem[] {
577+
return this.items
578+
}
579+
557580
push(...values: TItem[]) {
558581
const items = this.items
559582
items.push(...values)
@@ -594,7 +617,7 @@ export class DynamicArrayImpl<TItem extends ARC4Encoded> extends DynamicArray<TI
594617
export class TupleImpl<TTuple extends [ARC4Encoded, ...ARC4Encoded[]]> extends Tuple<TTuple> {
595618
private value?: TTuple
596619
private uint8ArrayValue?: Uint8Array
597-
private typeInfo: TypeInfo
620+
typeInfo: TypeInfo
598621
genericArgs: TypeInfo[]
599622

600623
constructor(typeInfo: TypeInfo | string)
@@ -691,7 +714,6 @@ export class TupleImpl<TTuple extends [ARC4Encoded, ...ARC4Encoded[]]> extends T
691714
type StructConstraint = Record<string, ARC4Encoded>
692715
export class StructImpl<T extends StructConstraint> extends (Struct<StructConstraint> as DeliberateAny) {
693716
private uint8ArrayValue?: Uint8Array
694-
private typeInfo: TypeInfo
695717
genericArgs: Record<string, TypeInfo>
696718

697719
constructor(typeInfo: TypeInfo | string, value: T = {} as T) {
@@ -737,6 +759,10 @@ export class StructImpl<T extends StructConstraint> extends (Struct<StructConstr
737759
return result as T
738760
}
739761

762+
get native(): T {
763+
return this.items
764+
}
765+
740766
private decodeAsProperties() {
741767
if (this.uint8ArrayValue) {
742768
const values = decode(this.uint8ArrayValue, Object.values(this.genericArgs))
@@ -769,7 +795,7 @@ export class StructImpl<T extends StructConstraint> extends (Struct<StructConstr
769795
}
770796

771797
export class DynamicBytesImpl extends DynamicBytes {
772-
private typeInfo: TypeInfo
798+
typeInfo: TypeInfo
773799
private value: DynamicArrayImpl<ByteImpl>
774800

775801
constructor(typeInfo: TypeInfo | string, value?: bytes | string) {
@@ -825,7 +851,7 @@ export class DynamicBytesImpl extends DynamicBytes {
825851

826852
export class StaticBytesImpl extends StaticBytes {
827853
private value: StaticArrayImpl<ByteImpl, number>
828-
private typeInfo: TypeInfo
854+
typeInfo: TypeInfo
829855

830856
constructor(typeInfo: TypeInfo | string, value?: bytes | string) {
831857
super(value)
@@ -1161,3 +1187,85 @@ export const getArc4TypeName = (typeInfo: TypeInfo): string | undefined => {
11611187
else if (typeof name === 'function') return name(typeInfo)
11621188
return undefined
11631189
}
1190+
1191+
export function decodeArc4Impl<T>(sourceTypeInfoString: string, bytes: internal.primitives.StubBytesCompat): T {
1192+
const sourceTypeInfo = JSON.parse(sourceTypeInfoString)
1193+
const encoder = getArc4Encoder(sourceTypeInfo)
1194+
const source = encoder(bytes, sourceTypeInfo)
1195+
1196+
return getNativeValue(source) as T
1197+
}
1198+
1199+
export function encodeArc4Impl<T>(_targetTypeInfoString: string, source: T): bytes {
1200+
const arc4Encoded = getArc4Encoded(source)
1201+
return arc4Encoded.bytes
1202+
}
1203+
1204+
const getNativeValue = (value: DeliberateAny): DeliberateAny => {
1205+
const native = (value as DeliberateAny).native
1206+
if (Array.isArray(native)) {
1207+
return native.map((item) => getNativeValue(item))
1208+
} else if (native instanceof internal.primitives.AlgoTsPrimitiveCls) {
1209+
return native
1210+
} else if (typeof native === 'object') {
1211+
return Object.fromEntries(Object.entries(native).map(([key, value]) => [key, getNativeValue(value)]))
1212+
}
1213+
return native
1214+
}
1215+
1216+
const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
1217+
if (value instanceof ARC4Encoded) {
1218+
return value
1219+
}
1220+
if (value instanceof AccountCls) {
1221+
const index = (lazyContext.activeGroup.activeTransaction as ApplicationTransaction).apat.indexOf(value)
1222+
return new UintNImpl({ name: 'UintN<64>', genericArgs: [{ name: '64' }] }, asBigInt(index))
1223+
}
1224+
if (value instanceof AssetCls) {
1225+
const index = (lazyContext.activeGroup.activeTransaction as ApplicationTransaction).apas.indexOf(value)
1226+
return new UintNImpl({ name: 'UintN<64>', genericArgs: [{ name: '64' }] }, asBigInt(index))
1227+
}
1228+
if (value instanceof ApplicationCls) {
1229+
const index = (lazyContext.activeGroup.activeTransaction as ApplicationTransaction).apfa.indexOf(value)
1230+
return new UintNImpl({ name: 'UintN<64>', genericArgs: [{ name: '64' }] }, asBigInt(index))
1231+
}
1232+
if (typeof value === 'boolean') {
1233+
return new BoolImpl({ name: 'Bool' }, value)
1234+
}
1235+
if (value instanceof internal.primitives.Uint64Cls || typeof value === 'number') {
1236+
return new UintNImpl({ name: 'UintN<64>', genericArgs: [{ name: '64' }] }, asBigInt(value))
1237+
}
1238+
if (value instanceof internal.primitives.BigUintCls) {
1239+
return new UintNImpl({ name: 'UintN<512>', genericArgs: [{ name: '512' }] }, value.asBigInt())
1240+
}
1241+
if (typeof value === 'bigint') {
1242+
return new UintNImpl({ name: 'UintN<512>', genericArgs: [{ name: '512' }] }, value)
1243+
}
1244+
if (value instanceof internal.primitives.BytesCls) {
1245+
return new DynamicBytesImpl(
1246+
{ name: 'DynamicBytes', genericArgs: { elementType: { name: 'Byte', genericArgs: [{ name: '8' }] } } },
1247+
value.asAlgoTs(),
1248+
)
1249+
}
1250+
if (typeof value === 'string') {
1251+
return new StrImpl({ name: 'Str' }, value)
1252+
}
1253+
if (Array.isArray(value)) {
1254+
const result: ARC4Encoded[] = value.reduce((acc: ARC4Encoded[], cur: DeliberateAny) => {
1255+
return acc.concat(getArc4Encoded(cur))
1256+
}, [])
1257+
const genericArgs: TypeInfo[] = result.map((x) => (x as DeliberateAny).typeInfo)
1258+
const typeInfo = { name: `Tuple<[${genericArgs.map((x) => x.name).join(',')}]>`, genericArgs }
1259+
return new TupleImpl(typeInfo, ...(result as []))
1260+
}
1261+
if (typeof value === 'object') {
1262+
const result = Object.values(value).reduce((acc: ARC4Encoded[], cur: DeliberateAny) => {
1263+
return acc.concat(getArc4Encoded(cur))
1264+
}, [])
1265+
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]])) }
1267+
return new StructImpl(typeInfo, Object.fromEntries(Object.keys(value).map((x, i) => [x, result[i]])))
1268+
}
1269+
1270+
throw internal.errors.codeError(`Unsupported type for encoding: ${typeof value}`)
1271+
}

src/impl/transactions.ts

+9
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,15 @@ export class ApplicationTransaction extends TransactionBase implements gtxn.Appl
307307
get lastLog() {
308308
return this.#appLogs.at(-1) ?? lazyContext.getApplicationData(this.appId.id).appLogs.at(-1) ?? Bytes()
309309
}
310+
get apat() {
311+
return this.#accounts
312+
}
313+
get apas() {
314+
return this.#assets
315+
}
316+
get apfa() {
317+
return this.#apps
318+
}
310319
appArgs(index: internal.primitives.StubUint64Compat): bytes {
311320
return toBytes(this.#appArgs[asNumber(index)])
312321
}

src/runtime-helpers.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { nameOfType } from './util'
88

99
export { attachAbiMetadata } from './abi-metadata'
1010
export * from './impl/encoded-types'
11+
export { decodeArc4Impl, encodeArc4Impl } from './impl/encoded-types'
1112
export { ensureBudgetImpl } from './impl/ensure-budget'
1213
export { TemplateVarImpl } from './impl/template-var'
1314

src/test-transformer/node-factory.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,19 @@ export const nodeFactory = {
9494
)
9595
},
9696

97-
callStubbedFunction(functionName: string, node: ts.CallExpression, typeInfo?: TypeInfo) {
98-
const infoString = JSON.stringify(typeInfo)
97+
callStubbedFunction(functionName: string, node: ts.CallExpression, ...typeInfos: (TypeInfo | undefined)[]) {
98+
const infoStringArray = typeInfos.length ? typeInfos.map((typeInfo) => JSON.stringify(typeInfo)) : undefined
9999
const updatedPropertyAccessExpression = factory.createPropertyAccessExpression(
100100
factory.createIdentifier('runtimeHelpers'),
101101
`${functionName}Impl`,
102102
)
103-
103+
const typeInfoArgs = infoStringArray
104+
? infoStringArray?.filter((s) => !!s).map((infoString) => factory.createStringLiteral(infoString))
105+
: undefined
104106
return factory.createCallExpression(
105107
updatedPropertyAccessExpression,
106108
node.typeArguments,
107-
[infoString ? factory.createStringLiteral(infoString) : undefined, ...(node.arguments ?? [])].filter((arg) => !!arg),
109+
[...(typeInfoArgs ?? []), ...(node.arguments ?? [])].filter((arg) => !!arg),
108110
)
109111
},
110112
} satisfies Record<string, (...args: DeliberateAny[]) => ts.Node>

src/test-transformer/visitors.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,13 @@ class ExpressionVisitor {
111111
}
112112
if (ts.isCallExpression(updatedNode)) {
113113
const stubbedFunctionName = tryGetStubbedFunctionName(updatedNode, this.helper)
114-
updatedNode = stubbedFunctionName ? nodeFactory.callStubbedFunction(stubbedFunctionName, updatedNode, info) : updatedNode
114+
const infos = [info]
115+
if (isCallingDecodeArc4(stubbedFunctionName)) {
116+
const targetType = ptypes.ptypeToArc4EncodedType(type, this.helper.sourceLocation(node))
117+
const targetTypeInfo = getGenericTypeInfo(targetType)
118+
infos[0] = targetTypeInfo
119+
}
120+
updatedNode = stubbedFunctionName ? nodeFactory.callStubbedFunction(stubbedFunctionName, updatedNode, ...infos) : updatedNode
115121
}
116122
return isGeneric
117123
? nodeFactory.captureGenericTypeInfo(ts.visitEachChild(updatedNode, this.visit, this.context), JSON.stringify(info))
@@ -289,6 +295,7 @@ const isGenericType = (type: ptypes.PType): boolean =>
289295
ptypes.StaticArrayType,
290296
ptypes.UFixedNxMType,
291297
ptypes.UintNType,
298+
ptypes.TuplePType,
292299
)
293300

294301
const isArc4EncodedType = (type: ptypes.PType): boolean =>
@@ -356,3 +363,5 @@ const tryGetStubbedFunctionName = (node: ts.CallExpression, helper: VisitorHelpe
356363
const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'TemplateVar', 'ensureBudget']
357364
return stubbedFunctionNames.includes(functionName) ? functionName : undefined
358365
}
366+
367+
const isCallingDecodeArc4 = (functionName: string | undefined): boolean => ['decodeArc4', 'encodeArc4'].includes(functionName ?? '')

src/value-generators/arc4.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ export class Arc4ValueGenerator {
1111
* */
1212
address(): arc4.Address {
1313
const source = new AvmValueGenerator().account()
14-
const result = new AddressImpl({ name: 'StaticArray', genericArgs: { elementType: { name: 'Byte' }, size: { name: '32' } } }, source)
14+
const result = new AddressImpl(
15+
{ name: 'StaticArray', genericArgs: { elementType: { name: 'Byte', genericArgs: [{ name: '8' }] }, size: { name: '32' } } },
16+
source,
17+
)
1518
return result
1619
}
1720

@@ -93,7 +96,7 @@ export class Arc4ValueGenerator {
9396
* */
9497
dynamicBytes(n: number): arc4.DynamicBytes {
9598
return new DynamicBytesImpl(
96-
{ name: 'DynamicArray', genericArgs: { elementType: { name: 'Byte' } } },
99+
{ name: 'DynamicBytes', genericArgs: { elementType: { name: 'Byte', genericArgs: [{ name: '8' }] } } },
97100
getRandomBytes(n / BITS_IN_BYTE).asAlgoTs(),
98101
)
99102
}

0 commit comments

Comments
 (0)