Skip to content

Commit 2932aae

Browse files
committed
feat: allow tuples and objects to be stored in box, local, and global state
1 parent 6032c0f commit 2932aae

File tree

7 files changed

+131
-3
lines changed

7 files changed

+131
-3
lines changed

src/impl/encoded-types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ export class StructImpl<T extends StructConstraint> extends (Struct<StructConstr
782782
return new Proxy(this, {
783783
get(target, prop) {
784784
const originalValue = Reflect.get(target, prop)
785-
if (originalValue === undefined && target.uint8ArrayValue && Object.keys(target.genericArgs).includes(prop.toString())) {
785+
if (originalValue === undefined && target.uint8ArrayValue?.length && Object.keys(target.genericArgs).includes(prop.toString())) {
786786
return target.items[prop.toString()]
787787
}
788788
return originalValue
@@ -1268,6 +1268,7 @@ export const arc4Encoders: Record<string, fromBytes<DeliberateAny>> = {
12681268
'Struct(<.*>)?': StructImpl.fromBytesImpl,
12691269
DynamicBytes: DynamicBytesImpl.fromBytesImpl,
12701270
'StaticBytes<.*>': StaticBytesImpl.fromBytesImpl,
1271+
object: StructImpl.fromBytesImpl,
12711272
}
12721273
export const getArc4Encoder = <T>(typeInfo: TypeInfo, encoders?: Record<string, fromBytes<DeliberateAny>>): fromBytes<T> => {
12731274
const encoder = Object.entries(encoders ?? arc4Encoders).find(([k, _]) => new RegExp(`^${k}$`, 'i').test(typeInfo.name))?.[1]

src/test-transformer/visitors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,12 @@ const getGenericTypeInfo = (type: ptypes.PType): TypeInfo => {
407407
)
408408
} else if (type instanceof ptypes.ARC4TupleType || type instanceof ptypes.TuplePType) {
409409
genericArgs.push(...type.items.map(getGenericTypeInfo))
410+
} else if (type instanceof ptypes.ObjectPType) {
411+
genericArgs = Object.fromEntries(
412+
Object.entries(type.properties)
413+
.map(([key, value]) => [key, getGenericTypeInfo(value)])
414+
.filter((x) => !!x),
415+
)
410416
}
411417

412418
const result: TypeInfo = { name: typeName }

tests/artifacts/state-ops/contract.algo.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,8 @@ export class GlobalStateContract extends arc4.Contract {
605605
implicitKeyArc4Address = GlobalState({ initialValue: new Address(Global.creatorAddress) })
606606
implicitKeyArc4UintN128 = GlobalState({ initialValue: new UintN128(2n ** 100n) })
607607
implicitKeyArc4DynamicBytes = GlobalState({ initialValue: new DynamicBytes('dynamic bytes') })
608+
implicitKeyTuple = GlobalState<[uint64, bytes, boolean]>({ initialValue: [Uint64(42), Bytes('Hello'), false] })
609+
implicitKeyObj = GlobalState<{ a: uint64; b: bytes; c: boolean }>({ initialValue: { a: 42, b: Bytes('World'), c: true } })
608610

609611
// Explicit key state variables
610612
arc4UintN64 = GlobalState({ initialValue: new UintN64(1337), key: 'explicit_key_arc4_uintn64' })
@@ -651,6 +653,16 @@ export class GlobalStateContract extends arc4.Contract {
651653
return this.implicitKeyArc4DynamicBytes.value
652654
}
653655

656+
@arc4.abimethod()
657+
get_implicit_key_tuple(): [uint64, bytes, boolean] {
658+
return this.implicitKeyTuple.value
659+
}
660+
661+
@arc4.abimethod()
662+
get_implicit_key_obj(): { a: uint64; b: bytes; c: boolean } {
663+
return this.implicitKeyObj.value
664+
}
665+
654666
// Getter methods for explicit key state variables
655667
@arc4.abimethod()
656668
get_arc4_uintn64(): UintN64 {
@@ -723,6 +735,16 @@ export class GlobalStateContract extends arc4.Contract {
723735
this.implicitKeyArc4DynamicBytes.value = value
724736
}
725737

738+
@arc4.abimethod()
739+
set_implicit_key_tuple(value: [uint64, bytes, boolean]) {
740+
this.implicitKeyTuple.value = value
741+
}
742+
743+
@arc4.abimethod()
744+
set_implicit_key_obj(value: { a: uint64; b: bytes; c: boolean }) {
745+
this.implicitKeyObj.value = value
746+
}
747+
726748
// Setter methods for explicit key state variables
727749
@arc4.abimethod()
728750
set_arc4_uintn64(value: UintN64) {
@@ -769,6 +791,8 @@ export class LocalStateContract extends arc4.Contract {
769791
implicitKeyArc4Address = LocalState<Address>()
770792
implicitKeyArc4UintN128 = LocalState<UintN128>()
771793
implicitKeyArc4DynamicBytes = LocalState<DynamicBytes>()
794+
implicitKeyTuple = LocalState<[uint64, bytes, boolean]>()
795+
implicitKeyObj = LocalState<{ a: uint64; b: bytes; c: boolean }>()
772796

773797
// Explicit key state variables
774798
arc4UintN64 = LocalState<UintN64>({ key: 'explicit_key_arc4_uintn64' })
@@ -788,6 +812,8 @@ export class LocalStateContract extends arc4.Contract {
788812
this.implicitKeyArc4Address(Global.creatorAddress).value = new Address(Global.creatorAddress)
789813
this.implicitKeyArc4UintN128(Global.creatorAddress).value = new UintN128(2n ** 100n)
790814
this.implicitKeyArc4DynamicBytes(Global.creatorAddress).value = new DynamicBytes('dynamic bytes')
815+
this.implicitKeyTuple(Global.creatorAddress).value = [42, Bytes('dummy_bytes'), true]
816+
this.implicitKeyObj(Global.creatorAddress).value = { a: Uint64(42), b: Bytes('dummy_bytes'), c: true }
791817

792818
this.arc4UintN64(Global.creatorAddress).value = new UintN64(1337)
793819
this.arc4Str(Global.creatorAddress).value = new Str('Hello')
@@ -834,6 +860,16 @@ export class LocalStateContract extends arc4.Contract {
834860
return this.implicitKeyArc4DynamicBytes(a).value
835861
}
836862

863+
@arc4.abimethod()
864+
get_implicit_key_tuple(a: Account): [uint64, bytes, boolean] {
865+
return this.implicitKeyTuple(a).value
866+
}
867+
868+
@arc4.abimethod()
869+
get_implicit_key_obj(a: Account): { a: uint64; b: bytes; c: boolean } {
870+
return this.implicitKeyObj(a).value
871+
}
872+
837873
// Getter methods for explicit key state variables
838874
@arc4.abimethod()
839875
get_arc4_uintn64(a: Account): arc4.UintN64 {

tests/global-state-arc4-values.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Bytes } from '@algorandfoundation/algorand-typescript'
1+
import { Bytes, Uint64 } from '@algorandfoundation/algorand-typescript'
22
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
33
import type {
44
AddressImpl,
@@ -106,6 +106,22 @@ describe('ARC4 AppGlobal values', async () => {
106106
expect(arc4Value.native).toEqual(expectedValue)
107107
},
108108
},
109+
{
110+
nativeValue: [21, asUint8Array(Bytes('Hello')), true],
111+
abiValue: [Uint64(21), Bytes('Hello'), true],
112+
methodName: `get_implicit_key_tuple`,
113+
assert: (value: DeliberateAny, expectedValue: DeliberateAny) => {
114+
expect(value).toEqual(expectedValue)
115+
},
116+
},
117+
{
118+
nativeValue: { a: 12, b: asUint8Array(Bytes('world')), c: true },
119+
abiValue: { a: 12, b: Bytes('world'), c: true },
120+
methodName: `get_implicit_key_obj`,
121+
assert: (value: DeliberateAny, expectedValue: DeliberateAny) => {
122+
expect(value).toEqual(expectedValue)
123+
},
124+
},
109125
])
110126

111127
test.for(testData)('should be able to get arc4 state values', async (data, { appClientGlobalStateContract: appClient, testAccount }) => {

tests/local-state-arc4-values.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ describe('ARC4 AppLocal values', async () => {
9393
expect(arc4Value.native).toEqual(expectedValue)
9494
},
9595
},
96+
{
97+
methodName: `get_implicit_key_tuple`,
98+
assert: (value: DeliberateAny, expectedValue: DeliberateAny) => {
99+
expect(value).toEqual(expectedValue)
100+
},
101+
},
102+
{
103+
methodName: `get_implicit_key_obj`,
104+
assert: (value: DeliberateAny, expectedValue: DeliberateAny) => {
105+
expect(value).toEqual(expectedValue)
106+
},
107+
},
96108
])
97109

98110
test.for(testData)('should be able to get arc4 state values', async (data, { appClientLocalStateContract: appClient, testAccount }) => {

tests/references/box-map.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import type { biguint, bytes, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { BigUint, BoxMap, Bytes, op, Uint64 } from '@algorandfoundation/algorand-typescript'
33
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
4-
import { ARC4Encoded, DynamicArray, interpretAsArc4, Str, UintN64 } from '@algorandfoundation/algorand-typescript/arc4'
4+
import type { Bool, DynamicBytes, Tuple } from '@algorandfoundation/algorand-typescript/arc4'
5+
import { ARC4Encoded, DynamicArray, interpretAsArc4, Str, Struct, UintN64 } from '@algorandfoundation/algorand-typescript/arc4'
56
import { afterEach, describe, expect, it, test } from 'vitest'
67
import { MAX_UINT64 } from '../../src/constants'
78
import { toBytes } from '../../src/encoders'
89
import { asBytes } from '../../src/util'
910

1011
const BOX_NOT_CREATED_ERROR = 'Box has not been created'
1112

13+
class MyStruct extends Struct<{ a: Str; b: DynamicBytes; c: Bool }> {}
14+
1215
describe('BoxMap', () => {
1316
const ctx = new TestExecutionContext()
1417
const keyPrefix = Bytes('test_key_prefix')
@@ -97,6 +100,30 @@ describe('BoxMap', () => {
97100
})
98101
},
99102
},
103+
{
104+
key: new Str('TTest'),
105+
value: ['hello', Bytes('world'), true] as const,
106+
newValue: ['world', Bytes('hello'), false] as const,
107+
emptyValue: interpretAsArc4<Tuple<[Str, DynamicBytes, Bool]>>(Bytes('')),
108+
withBoxContext: (test: (boxMap: BoxMap<Str, readonly [string, bytes, boolean]>) => void) => {
109+
ctx.txn.createScope([ctx.any.txn.applicationCall()]).execute(() => {
110+
const boxMap = BoxMap<Str, readonly [string, bytes, boolean]>({ keyPrefix })
111+
test(boxMap)
112+
})
113+
},
114+
},
115+
{
116+
key: new Str('OTest'),
117+
value: { a: 'hello', b: Bytes('world'), c: true } as unknown as MyStruct,
118+
newValue: { a: 'world', b: Bytes('hello'), c: false } as unknown as MyStruct,
119+
emptyValue: interpretAsArc4<MyStruct>(Bytes('')),
120+
withBoxContext: (test: (boxMap: BoxMap<Str, MyStruct>) => void) => {
121+
ctx.txn.createScope([ctx.any.txn.applicationCall()]).execute(() => {
122+
const boxMap = BoxMap<Str, MyStruct>({ keyPrefix })
123+
test(boxMap)
124+
})
125+
},
126+
},
100127
]
101128

102129
afterEach(() => {
@@ -199,6 +226,8 @@ describe('BoxMap', () => {
199226
expect(opContent).toEqual(newBytesValue)
200227
if (newValue instanceof ARC4Encoded) {
201228
expect((boxMap(key as never).value as ARC4Encoded).bytes).toEqual(newValue.bytes)
229+
} else if (boxMap(key as never).value instanceof ARC4Encoded) {
230+
expect((boxMap(key as never).value as ARC4Encoded).bytes).toEqual(newBytesValue)
202231
} else {
203232
expect(boxMap(key as never).value).toEqual(newValue)
204233
}

tests/references/box.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { biguint, bytes, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { BigUint, Box, Bytes, op, Uint64 } from '@algorandfoundation/algorand-typescript'
33
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
4+
import type { DynamicBytes } from '@algorandfoundation/algorand-typescript/arc4'
45
import {
56
ARC4Encoded,
67
Bool,
78
DynamicArray,
89
interpretAsArc4,
910
StaticArray,
1011
Str,
12+
Struct,
1113
Tuple,
1214
UintN32,
1315
UintN64,
@@ -22,6 +24,8 @@ import { BoxContract } from '../artifacts/box-contract/contract.algo'
2224

2325
const BOX_NOT_CREATED_ERROR = 'Box has not been created'
2426

27+
class MyStruct extends Struct<{ a: Str; b: DynamicBytes; c: Bool }> {}
28+
2529
describe('Box', () => {
2630
const ctx = new TestExecutionContext()
2731
const key = Bytes('test_key')
@@ -103,6 +107,28 @@ describe('Box', () => {
103107
})
104108
},
105109
},
110+
{
111+
value: ['hello', Bytes('world'), true] as const,
112+
newValue: ['world', Bytes('hello'), false] as const,
113+
emptyValue: interpretAsArc4<Tuple<[Str, DynamicBytes, Bool]>>(Bytes('')),
114+
withBoxContext: (test: (boxMap: Box<readonly [string, bytes, boolean]>) => void) => {
115+
ctx.txn.createScope([ctx.any.txn.applicationCall()]).execute(() => {
116+
const boxMap = Box<readonly [string, bytes, boolean]>({ key })
117+
test(boxMap)
118+
})
119+
},
120+
},
121+
{
122+
value: { a: 'hello', b: Bytes('world'), c: true },
123+
newValue: { a: 'world', b: Bytes('hello'), c: false },
124+
emptyValue: interpretAsArc4<MyStruct>(Bytes('')),
125+
withBoxContext: (test: (boxMap: Box<{ a: string; b: bytes; c: boolean }>) => void) => {
126+
ctx.txn.createScope([ctx.any.txn.applicationCall()]).execute(() => {
127+
const boxMap = Box<{ a: string; b: bytes; c: boolean }>({ key })
128+
test(boxMap)
129+
})
130+
},
131+
},
106132
]
107133

108134
afterEach(() => {
@@ -211,6 +237,8 @@ describe('Box', () => {
211237
expect(opContent).toEqual(newBytesValue)
212238
if (newValue instanceof ARC4Encoded) {
213239
expect((box as DeliberateAny).get(key).bytes).toEqual(newValue.bytes)
240+
} else if (box.value instanceof ARC4Encoded) {
241+
expect(box.value.bytes).toEqual(newBytesValue)
214242
} else {
215243
expect(box.value).toEqual(newValue)
216244
}

0 commit comments

Comments
 (0)