Skip to content

Commit 51d68f6

Browse files
committed
feat: implement stubs for encode/decode native arrays
1 parent 9fb1668 commit 51d68f6

File tree

5 files changed

+135
-25
lines changed

5 files changed

+135
-25
lines changed

src/encoders.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const toBytes = (val: unknown): bytes => {
8585
return val.bytes
8686
}
8787
if (Array.isArray(val) || typeof val === 'object') {
88-
return encodeArc4Impl('', val)
88+
return encodeArc4Impl(undefined, val)
8989
}
9090
throw new InternalError(`Invalid type for bytes: ${nameOfType(val)}`)
9191
}

src/impl/encoded-types.ts

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
conactUint8Arrays,
4949
uint8ArrayToNumber,
5050
} from '../util'
51+
import { BytesBackedCls, Uint64BackedCls } from './base'
5152
import type { StubBytesCompat } from './primitives'
5253
import { AlgoTsPrimitiveCls, arrayUtil, BigUintCls, Bytes, BytesCls, getUint8Array, isBytes, Uint64Cls } from './primitives'
5354
import { Account, AccountCls, ApplicationCls, AssetCls } from './reference'
@@ -1297,31 +1298,49 @@ export const getArc4TypeName = (typeInfo: TypeInfo): string | undefined => {
12971298
return undefined
12981299
}
12991300

1300-
export function decodeArc4Impl<T>(sourceTypeInfoString: string, bytes: StubBytesCompat, prefix: 'none' | 'log' = 'none'): T {
1301+
export function decodeArc4Impl<T>(
1302+
sourceTypeInfoString: string,
1303+
targetTypeInfoString: string,
1304+
bytes: StubBytesCompat,
1305+
prefix: 'none' | 'log' = 'none',
1306+
): T {
13011307
const sourceTypeInfo = JSON.parse(sourceTypeInfoString)
1308+
const targetTypeInfo = JSON.parse(targetTypeInfoString)
13021309
const encoder = getArc4Encoder(sourceTypeInfo)
1303-
const source = encoder(bytes, sourceTypeInfo, prefix)
1304-
return getNativeValue(source) as T
1310+
const source = encoder(bytes, sourceTypeInfo, prefix) as { typeInfo: TypeInfo }
1311+
return getNativeValue(source, targetTypeInfo) as T
13051312
}
13061313

1307-
export function encodeArc4Impl<T>(_targetTypeInfoString: string | undefined, source: T): bytes {
1308-
const arc4Encoded = getArc4Encoded(source)
1314+
export function encodeArc4Impl<T>(sourceTypeInfoString: string | undefined, source: T): bytes {
1315+
const arc4Encoded = getArc4Encoded(source, sourceTypeInfoString)
13091316
return arc4Encoded.bytes
13101317
}
13111318

1312-
const getNativeValue = (value: DeliberateAny): DeliberateAny => {
1319+
const getNativeValue = (value: DeliberateAny, targetTypeInfo: TypeInfo | undefined): DeliberateAny => {
1320+
if (value.typeInfo && value.typeInfo.name === targetTypeInfo?.name) {
1321+
return value
1322+
}
13131323
const native = (value as DeliberateAny).native
13141324
if (Array.isArray(native)) {
1315-
return native.map((item) => getNativeValue(item))
1325+
return native.map((item) => getNativeValue(item, (targetTypeInfo?.genericArgs as { elementType: TypeInfo })?.elementType))
13161326
} else if (native instanceof AlgoTsPrimitiveCls) {
13171327
return native
1328+
} else if (native instanceof BytesBackedCls) {
1329+
return native.bytes
1330+
} else if (native instanceof Uint64BackedCls) {
1331+
return native.uint64
13181332
} else if (typeof native === 'object') {
1319-
return Object.fromEntries(Object.entries(native).map(([key, value]) => [key, getNativeValue(value)]))
1333+
return Object.fromEntries(
1334+
Object.entries(native).map(([key, value], index) => [
1335+
key,
1336+
getNativeValue(value, (targetTypeInfo?.genericArgs as TypeInfo[])?.[index]),
1337+
]),
1338+
)
13201339
}
13211340
return native
13221341
}
13231342

1324-
export const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
1343+
export const getArc4Encoded = (value: DeliberateAny, sourceTypeInfoString?: string): ARC4Encoded => {
13251344
if (value instanceof ARC4Encoded) {
13261345
return value
13271346
}
@@ -1362,9 +1381,15 @@ export const getArc4Encoded = (value: DeliberateAny): ARC4Encoded => {
13621381
const result: ARC4Encoded[] = value.reduce((acc: ARC4Encoded[], cur: DeliberateAny) => {
13631382
return acc.concat(getArc4Encoded(cur))
13641383
}, [])
1384+
const sourceTypeInfo = sourceTypeInfoString ? JSON.parse(sourceTypeInfoString) : undefined
13651385
const genericArgs: TypeInfo[] = result.map((x) => (x as DeliberateAny).typeInfo)
1366-
const typeInfo = { name: `Tuple<[${genericArgs.map((x) => x.name).join(',')}]>`, genericArgs }
1367-
return new TupleImpl(typeInfo, ...(result as [ARC4Encoded, ...ARC4Encoded[]]))
1386+
if (sourceTypeInfo?.name?.startsWith('Array')) {
1387+
const typeInfo = { name: `DynamicArray<${genericArgs[0].name}>`, genericArgs: { elementType: genericArgs[0] } }
1388+
return new DynamicArrayImpl(typeInfo, ...(result as [ARC4Encoded, ...ARC4Encoded[]]))
1389+
} else {
1390+
const typeInfo = { name: `Tuple<[${genericArgs.map((x) => x.name).join(',')}]>`, genericArgs }
1391+
return new TupleImpl(typeInfo, ...(result as [ARC4Encoded, ...ARC4Encoded[]]))
1392+
}
13681393
}
13691394
if (typeof value === 'object') {
13701395
const result = Object.values(value).reduce((acc: ARC4Encoded[], cur: DeliberateAny) => {

src/test-transformer/node-factory.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@ export const nodeFactory = {
9494
)
9595
},
9696

97-
callStubbedFunction(functionName: string, node: ts.CallExpression, typeInfo?: TypeInfo) {
98-
const typeInfoArg = typeInfo ? factory.createStringLiteral(JSON.stringify(typeInfo)) : undefined
97+
callStubbedFunction(functionName: string, node: ts.CallExpression, typeInfo?: TypeInfo | TypeInfo[]) {
98+
const typeInfoArgs = typeInfo
99+
? (Array.isArray(typeInfo) ? typeInfo : [typeInfo]).map((t) => factory.createStringLiteral(JSON.stringify(t)))
100+
: undefined
99101
const updatedPropertyAccessExpression = factory.createPropertyAccessExpression(
100102
factory.createIdentifier('runtimeHelpers'),
101103
`${functionName}Impl`,
@@ -104,7 +106,7 @@ export const nodeFactory = {
104106
return factory.createCallExpression(
105107
updatedPropertyAccessExpression,
106108
node.typeArguments,
107-
[typeInfoArg, ...(node.arguments ?? [])].filter((arg) => !!arg),
109+
[...(typeInfoArgs ?? []), ...(node.arguments ?? [])].filter((arg) => !!arg),
108110
)
109111
},
110112

src/test-transformer/visitors.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,16 @@ class ExpressionVisitor {
159159
if (ts.isCallExpression(updatedNode)) {
160160
const stubbedFunctionName = this.stubbedFunctionName ?? tryGetStubbedFunctionName(updatedNode, this.helper)
161161
this.stubbedFunctionName = undefined
162-
let infoArg = info
162+
let infoArg: TypeInfo | TypeInfo[] | undefined = info
163163
if (isCallingEmit(stubbedFunctionName)) {
164164
infoArg = this.helper.resolveTypeParameters(updatedNode).map(getGenericTypeInfo)[0]
165165
} else if (isCallingDecodeArc4(stubbedFunctionName)) {
166-
const targetType = ptypes.ptypeToArc4EncodedType(type, this.helper.sourceLocation(node))
167-
const targetTypeInfo = getGenericTypeInfo(targetType)
168-
infoArg = targetTypeInfo
166+
const sourceType = ptypes.ptypeToArc4EncodedType(type, this.helper.sourceLocation(node))
167+
const sourceTypeInfo = getGenericTypeInfo(sourceType)
168+
const targetTypeInfo = getGenericTypeInfo(type)
169+
infoArg = [sourceTypeInfo, targetTypeInfo]
170+
} else if (isCallingEncodeArc4(stubbedFunctionName)) {
171+
infoArg = this.helper.resolveTypeParameters(updatedNode).map(getGenericTypeInfo)[0]
169172
} else if (isCallingArc4EncodedLength(stubbedFunctionName)) {
170173
infoArg = this.helper.resolveTypeParameters(updatedNode).map(getGenericTypeInfo)[0]
171174
}
@@ -384,7 +387,7 @@ const getGenericTypeInfo = (type: ptypes.PType): TypeInfo => {
384387
} else if (type instanceof ptypes.BoxMapPType) {
385388
genericArgs.push(getGenericTypeInfo(type.keyType))
386389
genericArgs.push(getGenericTypeInfo(type.contentType))
387-
} else if (instanceOfAny(type, ptypes.StaticArrayType, ptypes.DynamicArrayType)) {
390+
} else if (instanceOfAny(type, ptypes.StaticArrayType, ptypes.DynamicArrayType, ptypes.ArrayPType)) {
388391
const entries = []
389392
entries.push(['elementType', getGenericTypeInfo(type.elementType)])
390393
if (instanceOfAny(type, ptypes.StaticArrayType)) {
@@ -428,7 +431,8 @@ const tryGetStubbedFunctionName = (node: ts.CallExpression, helper: VisitorHelpe
428431
return stubbedFunctionNames.includes(functionName) ? functionName : undefined
429432
}
430433

431-
const isCallingDecodeArc4 = (functionName: string | undefined): boolean => ['decodeArc4', 'encodeArc4'].includes(functionName ?? '')
434+
const isCallingDecodeArc4 = (functionName: string | undefined): boolean => ['decodeArc4'].includes(functionName ?? '')
435+
const isCallingEncodeArc4 = (functionName: string | undefined): boolean => ['encodeArc4'].includes(functionName ?? '')
432436
const isCallingArc4EncodedLength = (functionName: string | undefined): boolean => 'arc4EncodedLength' === (functionName ?? '')
433437
const isCallingEmit = (functionName: string | undefined): boolean => 'emit' === (functionName ?? '')
434438
const isCallingMethodSelector = (functionName: string | undefined): boolean => 'methodSelector' === (functionName ?? '')

tests/arc4/encode-decode-arc4.spec.ts

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import type { biguint, bytes, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { Bytes } from '@algorandfoundation/algorand-typescript'
3-
import type { Address, StaticArray, StaticBytes, UFixedNxM, UintN64 } from '@algorandfoundation/algorand-typescript/arc4'
3+
import type { StaticBytes, UFixedNxM } from '@algorandfoundation/algorand-typescript/arc4'
44
import {
5+
Address,
56
arc4EncodedLength,
67
Bool,
78
decodeArc4,
9+
DynamicArray,
810
DynamicBytes,
911
encodeArc4,
12+
StaticArray,
1013
Str,
1114
Struct,
1215
Tuple,
1316
UintN,
17+
UintN64,
1418
} from '@algorandfoundation/algorand-typescript/arc4'
19+
import { itob } from '@algorandfoundation/algorand-typescript/op'
20+
import { encodingUtil } from '@algorandfoundation/puya-ts'
1521
import { describe, expect, test } from 'vitest'
1622
import { MAX_UINT128 } from '../../src/constants'
1723
import type { StubBytesCompat } from '../../src/impl/primitives'
@@ -30,6 +36,7 @@ const abiUint512 = new UintN<512>(MAX_UINT128)
3036
const abiBool = new Bool(true)
3137
const abiBytes = new DynamicBytes(Bytes('hello'))
3238

39+
type TestObj = { a: UintN64; b: DynamicBytes }
3340
class Swapped1 extends Struct<{
3441
b: UintN<64>
3542
c: Bool
@@ -40,14 +47,17 @@ class Swapped1 extends Struct<{
4047
const testData = [
4148
{
4249
nativeValues() {
43-
return [nativeNumber, nativeNumber, nativeBigInt, nativeBytes]
50+
return [nativeNumber, nativeNumber, nativeBigInt, nativeBytes] as readonly [uint64, uint64, biguint, bytes]
4451
},
4552
abiValues() {
4653
return [abiUint64, abiUint64, abiUint512, abiBytes] as readonly [UintN<64>, UintN<64>, UintN<512>, DynamicBytes]
4754
},
4855
arc4Value() {
4956
return new Tuple<[UintN<64>, UintN<64>, UintN<512>, DynamicBytes]>(abiUint64, abiUint64, abiUint512, abiBytes)
5057
},
58+
encode() {
59+
return encodeArc4(this.nativeValues())
60+
},
5161
decode(value: StubBytesCompat) {
5262
return decodeArc4<[uint64, uint64, biguint, bytes]>(asBytes(value))
5363
},
@@ -58,6 +68,19 @@ const testData = [
5868
[nativeBool, [nativeString, nativeBool]],
5969
[nativeNumber, nativeNumber],
6070
[nativeBigInt, nativeBytes, { b: nativeNumber, c: nativeBool, d: nativeString, a: [nativeNumber, nativeBool, nativeBool] }],
71+
] as readonly [
72+
[boolean, [string, boolean]],
73+
[uint64, uint64],
74+
[
75+
biguint,
76+
bytes,
77+
{
78+
b: uint64
79+
c: boolean
80+
d: string
81+
a: [uint64, boolean, boolean]
82+
},
83+
],
6184
]
6285
},
6386
abiValues() {
@@ -76,6 +99,9 @@ const testData = [
7699
...this.abiValues(),
77100
)
78101
},
102+
encode() {
103+
return encodeArc4(this.nativeValues())
104+
},
79105
decode(value: StubBytesCompat) {
80106
return decodeArc4<
81107
[
@@ -105,6 +131,9 @@ const testData = [
105131
arc4Value() {
106132
return new Swapped1(this.abiValues())
107133
},
134+
encode() {
135+
return encodeArc4(this.nativeValues())
136+
},
108137
decode(value: StubBytesCompat) {
109138
return decodeArc4<{ b: uint64; c: boolean; d: string; a: [uint64, boolean, boolean] }>(asBytes(value))
110139
},
@@ -120,17 +149,67 @@ describe('decodeArc4', () => {
120149

121150
compareNativeValues(result, nativeValues)
122151
})
152+
test('should be able to decode arrays', () => {
153+
const a = 234234
154+
const aBytes = asBytes(encodingUtil.bigIntToUint8Array(234234n, 8))
155+
const b = true
156+
const bBytes = asBytes(encodingUtil.bigIntToUint8Array(128n, 1))
157+
const c = 340943934n
158+
const cBytes = asBytes(encodingUtil.bigIntToUint8Array(340943934n, 512 / 8))
159+
const d = 'hello world'
160+
const dBytes = asBytes(
161+
new Uint8Array([
162+
...encodingUtil.bigIntToUint8Array(BigInt('hello world'.length), 2),
163+
...encodingUtil.utf8ToUint8Array('hello world'),
164+
]),
165+
)
166+
const e = { a: 50n, b: new Uint8Array([1, 2, 3, 4, 5]) }
167+
const eBytes = asBytes(new Uint8Array([...encodingUtil.bigIntToUint8Array(50n, 8), 0, 10, 0, 5, 1, 2, 3, 4, 5]))
168+
const f = new Address(Bytes.fromHex(`${'00'.repeat(31)}ff`))
169+
const fBytes = Bytes.fromHex(`${'00'.repeat(31)}ff`)
170+
expect(decodeArc4<uint64>(aBytes)).toEqual(a)
171+
expect(decodeArc4<boolean>(bBytes)).toEqual(b)
172+
expect(decodeArc4<biguint>(cBytes)).toEqual(c)
173+
expect(decodeArc4<string>(dBytes)).toEqual(d)
174+
expect(decodeArc4<TestObj>(eBytes)).toEqual(e)
175+
176+
const lenPrefix = itob(1).slice(6, 8)
177+
const offsetHeader = itob(2).slice(6, 8)
178+
expect(decodeArc4<uint64[]>(lenPrefix.concat(aBytes))).toEqual([a])
179+
expect(decodeArc4<boolean[]>(lenPrefix.concat(bBytes))).toEqual([b])
180+
expect(decodeArc4<biguint[]>(lenPrefix.concat(cBytes))).toEqual([c])
181+
expect(decodeArc4<string[]>(Bytes`${lenPrefix}${offsetHeader}${dBytes}`)).toEqual([d])
182+
expect(decodeArc4<TestObj[]>(Bytes`${lenPrefix}${offsetHeader}${eBytes}`)).toEqual([e])
183+
expect(JSON.stringify(decodeArc4<Address[]>(Bytes`${lenPrefix}${fBytes}`))).toEqual(JSON.stringify([f]))
184+
})
123185
})
124186

125187
describe('encodeArc4', () => {
126188
test.each(testData)('should encode native values', (data) => {
127-
const nativeValues = data.nativeValues()
128189
const arc4Value = data.arc4Value()
129190

130-
const result = encodeArc4(nativeValues)
191+
const result = data.encode()
131192

132193
expect(result).toEqual(arc4Value.bytes)
133194
})
195+
test('should be able to encode arrays', () => {
196+
const address = new Address(Bytes.fromHex(`${'00'.repeat(31)}ff`))
197+
expect(encodeArc4(address)).toEqual(address.bytes)
198+
199+
expect(encodeArc4([nativeNumber])).toEqual(new StaticArray(new UintN64(nativeNumber)).bytes)
200+
expect(encodeArc4([nativeBool])).toEqual(new StaticArray(new Bool(nativeBool)).bytes)
201+
expect(encodeArc4([nativeBigInt])).toEqual(new StaticArray(new UintN<512>(nativeBigInt)).bytes)
202+
expect(encodeArc4([nativeBytes])).toEqual(new StaticArray(new DynamicBytes(nativeBytes)).bytes)
203+
expect(encodeArc4([nativeString])).toEqual(new StaticArray(new Str(nativeString)).bytes)
204+
expect(encodeArc4([address])).toEqual(new StaticArray(address).bytes)
205+
206+
expect(encodeArc4<uint64[]>([nativeNumber])).toEqual(new DynamicArray(new UintN64(nativeNumber)).bytes)
207+
expect(encodeArc4<boolean[]>([nativeBool])).toEqual(new DynamicArray(new Bool(nativeBool)).bytes)
208+
expect(encodeArc4<biguint[]>([nativeBigInt])).toEqual(new DynamicArray(new UintN<512>(nativeBigInt)).bytes)
209+
expect(encodeArc4<bytes[]>([nativeBytes])).toEqual(new DynamicArray(new DynamicBytes(nativeBytes)).bytes)
210+
expect(encodeArc4<string[]>([nativeString])).toEqual(new DynamicArray(new Str(nativeString)).bytes)
211+
expect(encodeArc4<Address[]>([address])).toEqual(new DynamicArray(address).bytes)
212+
})
134213
})
135214

136215
class StaticStruct extends Struct<{

0 commit comments

Comments
 (0)