Skip to content

Commit 865e049

Browse files
committed
feat: using Uint8Array as backing store for all box values
- also: - expand generic type info being captured to include type info for all levels - capture generic type info for variables initialised in a function to make test code simpler to write for boxes - update box and box map implementations and tests to work with encoders
1 parent 9bc9ab1 commit 865e049

File tree

14 files changed

+386
-135
lines changed

14 files changed

+386
-135
lines changed

src/encoders.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { biguint, BigUint, bytes, Bytes, internal, TransactionType, uint64, Uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { OnCompleteAction } from '@algorandfoundation/algorand-typescript/arc4'
3+
import { AccountCls } from './impl/account'
4+
import { ApplicationCls } from './impl/application'
5+
import { AssetCls } from './impl/asset'
6+
7+
export interface GenericTypeInfo {
8+
name: string
9+
wtypeName?: string
10+
genericArgs?: GenericTypeInfo[]
11+
}
12+
13+
type fromBytes<T> = (val: Uint8Array, typeInfo: GenericTypeInfo) => T
14+
15+
const booleanFromBytes: fromBytes<boolean> = (val) => {
16+
return internal.encodingUtil.uint8ArrayToBigInt(val) > 0n
17+
}
18+
19+
const bigUintFromBytes: fromBytes<biguint> = (val) => {
20+
return BigUint(internal.encodingUtil.uint8ArrayToBigInt(val))
21+
}
22+
23+
const bytesFromBytes: fromBytes<bytes> = (val) => {
24+
return Bytes(val)
25+
}
26+
27+
const stringFromBytes: fromBytes<string> = (val) => {
28+
return Bytes(val).toString()
29+
}
30+
31+
const uint64FromBytes: fromBytes<uint64> = (val) => {
32+
return Uint64(internal.encodingUtil.uint8ArrayToBigInt(val))
33+
}
34+
35+
const onCompletionFromBytes: fromBytes<OnCompleteAction> = (val) => {
36+
return Uint64(internal.encodingUtil.uint8ArrayToBigInt(val)) as OnCompleteAction
37+
}
38+
39+
const transactionTypeFromBytes: fromBytes<TransactionType> = (val) => {
40+
return Uint64(internal.encodingUtil.uint8ArrayToBigInt(val)) as TransactionType
41+
}
42+
43+
export const encoders = {
44+
account: AccountCls.fromBytes,
45+
Account: AccountCls.fromBytes,
46+
application: ApplicationCls.fromBytes,
47+
Application: ApplicationCls.fromBytes,
48+
asset: AssetCls.fromBytes,
49+
Asset: AssetCls.fromBytes,
50+
bool: booleanFromBytes,
51+
boolean: booleanFromBytes,
52+
biguint: bigUintFromBytes,
53+
bytes: bytesFromBytes,
54+
string: stringFromBytes,
55+
uint64: uint64FromBytes,
56+
OnCompleteAction: onCompletionFromBytes,
57+
TransactionType: transactionTypeFromBytes,
58+
}
59+
60+
export const getEncoder = <T>(typeInfo: GenericTypeInfo): fromBytes<T> => {
61+
const encoder = encoders[typeInfo.name as keyof typeof encoders]
62+
if (!encoder) {
63+
throw new Error(`No encoder found for type ${typeInfo.name}`)
64+
}
65+
return encoder as fromBytes<T>
66+
}

src/impl/application.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class ApplicationData {
1313
appLogs: bytes[]
1414
globalStates: BytesMap<GlobalStateCls<unknown>>
1515
localStates: BytesMap<LocalState<unknown>>
16-
boxes: BytesMap<unknown>
16+
boxes: BytesMap<Uint8Array>
1717
}
1818
isCreating: boolean = false
1919

src/impl/base.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1-
import { bytes, uint64 } from '@algorandfoundation/algorand-typescript'
1+
import { Bytes, bytes, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { encodingUtil } from '@algorandfoundation/puya-ts'
3+
import type { GenericTypeInfo } from '../encoders'
24

35
export abstract class BytesBackedCls {
46
#value: bytes
7+
// #typeInfo: GenericTypeInfo | undefined
58

69
get bytes() {
710
return this.#value
811
}
9-
constructor(value: bytes) {
12+
constructor(value: bytes, _typeInfo?: GenericTypeInfo) {
1013
this.#value = value
14+
// this.#typeInfo = typeInfo
15+
}
16+
17+
static fromBytes<T extends BytesBackedCls>(
18+
this: { new (v: bytes, typeInfo?: GenericTypeInfo): T },
19+
value: Uint8Array,
20+
typeInfo?: GenericTypeInfo,
21+
) {
22+
return new this(Bytes(value), typeInfo)
1123
}
1224
}
1325

@@ -21,4 +33,9 @@ export abstract class Uint64BackedCls {
2133
constructor(value: uint64) {
2234
this.#value = value
2335
}
36+
37+
static fromBytes<T extends Uint64BackedCls>(this: { new (v: uint64): T }, value: Uint8Array) {
38+
const uint64Value = Uint64(encodingUtil.uint8ArrayToBigInt(value))
39+
return new this(uint64Value)
40+
}
2441
}

src/impl/box.ts

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { MAX_BOX_SIZE } from '../constants'
33
import { lazyContext } from '../context-helpers/internal-context'
4-
import { asBytes, asBytesCls, asNumber, conactUint8Arrays, toBytes } from '../util'
4+
import { asBytes, asBytesCls, asNumber, asUint8Array, conactUint8Arrays, toBytes } from '../util'
55

66
export const Box: internal.opTypes.BoxType = {
77
create(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubUint64Compat): boolean {
@@ -35,11 +35,7 @@ export const Box: internal.opTypes.BoxType = {
3535
throw new internal.errors.InternalError('Box does not exist')
3636
}
3737
const boxContent = lazyContext.ledger.getBox(app, name)
38-
if (boxContent instanceof Uint8Array) {
39-
return toBytes(boxContent.slice(start, start + length))
40-
}
41-
const result = toBytes(boxContent).slice(start, start + length)
42-
return result
38+
return toBytes(boxContent.slice(start, start + length))
4339
},
4440
get(a: internal.primitives.StubBytesCompat): readonly [bytes, boolean] {
4541
const name = asBytes(a)
@@ -52,43 +48,34 @@ export const Box: internal.opTypes.BoxType = {
5248
const app = lazyContext.activeApplication
5349
const boxContent = lazyContext.ledger.getBox(app, name)
5450
const exists = lazyContext.ledger.boxExists(app, name)
55-
if (boxContent instanceof Uint8Array) {
56-
return [boxContent.length, exists]
57-
}
58-
const bytesContent = toBytes(boxContent)
59-
return [bytesContent.length, exists]
51+
return [boxContent.length, exists]
6052
},
6153
put(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubBytesCompat): void {
6254
const name = asBytes(a)
6355
const app = lazyContext.activeApplication
64-
const newContent = asBytes(b)
56+
const newContent = asBytesCls(b)
6557
if (lazyContext.ledger.boxExists(app, name)) {
6658
const boxContent = lazyContext.ledger.getBox(app, name)
67-
const length = boxContent instanceof Uint8Array ? boxContent.length : toBytes(boxContent).length
59+
const length = boxContent.length
6860
if (asNumber(length) !== asNumber(newContent.length)) {
6961
throw new internal.errors.InternalError('New content length does not match existing box length')
7062
}
7163
}
72-
lazyContext.ledger.setBox(app, name, newContent)
64+
lazyContext.ledger.setBox(app, name, newContent.asUint8Array())
7365
},
7466
replace(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubUint64Compat, c: internal.primitives.StubBytesCompat): void {
7567
const name = asBytes(a)
7668
const start = asNumber(b)
77-
const newContent = asBytesCls(c).asUint8Array()
69+
const newContent = asUint8Array(c)
7870
const app = lazyContext.activeApplication
7971
if (!lazyContext.ledger.boxExists(app, name)) {
8072
throw new internal.errors.InternalError('Box does not exist')
8173
}
8274
const boxContent = lazyContext.ledger.getBox(app, name)
83-
const uint8ArrayContent = boxContent instanceof Uint8Array ? boxContent : asBytesCls(toBytes(boxContent)).asUint8Array()
84-
if (start + newContent.length > uint8ArrayContent.length) {
75+
if (start + newContent.length > boxContent.length) {
8576
throw new internal.errors.InternalError('Replacement content exceeds box size')
8677
}
87-
const updatedContent = conactUint8Arrays(
88-
uint8ArrayContent.slice(0, start),
89-
newContent,
90-
uint8ArrayContent.slice(start + newContent.length),
91-
)
78+
const updatedContent = conactUint8Arrays(boxContent.slice(0, start), newContent, boxContent.slice(start + newContent.length))
9279
lazyContext.ledger.setBox(app, name, updatedContent)
9380
},
9481
resize(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubUint64Compat): void {
@@ -99,13 +86,12 @@ export const Box: internal.opTypes.BoxType = {
9986
throw new internal.errors.InternalError('Box does not exist')
10087
}
10188
const boxContent = lazyContext.ledger.getBox(app, name)
102-
const uint8ArrayContent = boxContent instanceof Uint8Array ? boxContent : asBytesCls(toBytes(boxContent)).asUint8Array()
103-
const size = uint8ArrayContent.length
89+
const size = boxContent.length
10490
let updatedContent
10591
if (newSize > size) {
106-
updatedContent = conactUint8Arrays(uint8ArrayContent, new Uint8Array(Array(newSize - size).fill(0)))
92+
updatedContent = conactUint8Arrays(boxContent, new Uint8Array(Array(newSize - size).fill(0)))
10793
} else {
108-
updatedContent = uint8ArrayContent.slice(0, newSize)
94+
updatedContent = boxContent.slice(0, newSize)
10995
}
11096
lazyContext.ledger.setBox(app, name, updatedContent)
11197
},
@@ -118,19 +104,18 @@ export const Box: internal.opTypes.BoxType = {
118104
const name = asBytes(a)
119105
const start = asNumber(b)
120106
const length = asNumber(c)
121-
const newContent = asBytesCls(d).asUint8Array()
107+
const newContent = asUint8Array(d)
122108
const app = lazyContext.activeApplication
123109
if (!lazyContext.ledger.boxExists(app, name)) {
124110
throw new internal.errors.InternalError('Box does not exist')
125111
}
126112
const boxContent = lazyContext.ledger.getBox(app, name)
127-
const uint8ArrayContent = boxContent instanceof Uint8Array ? boxContent : asBytesCls(toBytes(boxContent)).asUint8Array()
128-
const size = uint8ArrayContent.length
113+
const size = boxContent.length
129114
if (start > size) {
130115
throw new internal.errors.InternalError('Start index exceeds box size')
131116
}
132117
const end = Math.min(start + length, size)
133-
let updatedContent = conactUint8Arrays(uint8ArrayContent.slice(0, start), newContent, uint8ArrayContent.slice(end))
118+
let updatedContent = conactUint8Arrays(boxContent.slice(0, start), newContent, boxContent.slice(end))
134119
// Adjust the size if necessary
135120
if (updatedContent.length > size) {
136121
updatedContent = updatedContent.slice(0, size)

src/impl/state.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
BoxMap as BoxMapType,
55
BoxRef as BoxRefType,
66
Box as BoxType,
7+
Bytes,
78
bytes,
89
GlobalStateOptions,
910
GlobalState as GlobalStateType,
@@ -16,7 +17,9 @@ import {
1617
import { AccountMap } from '../collections/custom-key-map'
1718
import { MAX_BOX_SIZE } from '../constants'
1819
import { lazyContext } from '../context-helpers/internal-context'
19-
import { asBytes, asBytesCls, asNumber, conactUint8Arrays, toBytes } from '../util'
20+
import { getEncoder } from '../encoders'
21+
import { getGenericTypeInfo } from '../runtime-helpers'
22+
import { asBytes, asBytesCls, asNumber, asUint8Array, conactUint8Arrays, toBytes } from '../util'
2023

2124
export class GlobalStateCls<ValueType> {
2225
private readonly _type: string = GlobalStateCls.name
@@ -126,14 +129,21 @@ export class BoxCls<TValue> {
126129
this.#app = lazyContext.activeApplication
127130
}
128131

132+
private get fromBytes() {
133+
const typeInfo = getGenericTypeInfo(this)
134+
const valueType = typeInfo!.genericArgs![0]
135+
return (val: Uint8Array) => getEncoder<TValue>(valueType)(val, valueType)
136+
}
137+
129138
get value(): TValue {
130139
if (!this.exists) {
131140
throw new internal.errors.InternalError('Box has not been created')
132141
}
133-
return lazyContext.ledger.getBox(this.#app, this.key)
142+
143+
return this.fromBytes(lazyContext.ledger.getBox(this.#app, this.key))
134144
}
135145
set value(v: TValue) {
136-
lazyContext.ledger.setBox(this.#app, this.key, v)
146+
lazyContext.ledger.setBox(this.#app, this.key, asUint8Array(toBytes(v)))
137147
}
138148

139149
get hasKey(): boolean {
@@ -172,7 +182,8 @@ export class BoxCls<TValue> {
172182
}
173183

174184
maybe(): readonly [TValue, boolean] {
175-
return [lazyContext.ledger.getBox(this.#app, this.key), lazyContext.ledger.boxExists(this.#app, this.key)]
185+
const value = this.fromBytes(lazyContext.ledger.getBox(this.#app, this.key))
186+
return [value, lazyContext.ledger.boxExists(this.#app, this.key)]
176187
}
177188
}
178189

@@ -186,6 +197,12 @@ export class BoxMapCls<TKey, TValue> {
186197
return x instanceof Object && '_type' in x && (x as { _type: string })['_type'] === BoxMapCls.name
187198
}
188199

200+
private get fromBytes() {
201+
const typeInfo = getGenericTypeInfo(this)
202+
const valueType = typeInfo!.genericArgs![1]
203+
return (val: Uint8Array) => getEncoder<TValue>(valueType)(val, valueType)
204+
}
205+
189206
constructor(keyPrefix?: internal.primitives.StubBytesCompat) {
190207
this.#keyPrefix = keyPrefix ? asBytes(keyPrefix) : undefined
191208
this.#app = lazyContext.activeApplication
@@ -215,7 +232,7 @@ export class BoxMapCls<TKey, TValue> {
215232
}
216233

217234
set(key: TKey, value: TValue): void {
218-
lazyContext.ledger.setBox(this.#app, this.getFullKey(key), value)
235+
lazyContext.ledger.setBox(this.#app, this.getFullKey(key), asUint8Array(toBytes(value)))
219236
}
220237

221238
delete(key: TKey): boolean {
@@ -228,7 +245,8 @@ export class BoxMapCls<TKey, TValue> {
228245

229246
maybe(key: TKey): readonly [TValue, boolean] {
230247
const fullKey = this.getFullKey(key)
231-
return [lazyContext.ledger.getBox(this.#app, fullKey), lazyContext.ledger.boxExists(this.#app, fullKey)]
248+
const value = this.fromBytes(lazyContext.ledger.getBox(this.#app, fullKey))
249+
return [value, lazyContext.ledger.boxExists(this.#app, fullKey)]
232250
}
233251

234252
length(key: TKey): uint64 {
@@ -394,7 +412,7 @@ export class BoxRefCls {
394412
}
395413

396414
maybe(): readonly [bytes, boolean] {
397-
return [asBytes(lazyContext.ledger.getBox(this.#app, this.key)), lazyContext.ledger.boxExists(this.#app, this.key)]
415+
return [Bytes(lazyContext.ledger.getBox(this.#app, this.key)), lazyContext.ledger.boxExists(this.#app, this.key)]
398416
}
399417

400418
get length(): uint64 {

src/runtime-helpers.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { internal } from '@algorandfoundation/algorand-typescript'
22
import { MAX_UINT64 } from './constants'
3+
import type { GenericTypeInfo } from './encoders'
34
import { DeliberateAny } from './typescript-helpers'
45
import { nameOfType } from './util'
56

@@ -273,12 +274,12 @@ function defaultUnaryOp(_operand: DeliberateAny, op: UnaryOps): DeliberateAny {
273274
internal.errors.internalError(`Unsupported operator ${op}`)
274275
}
275276

276-
const genericTypeMap = new Map<DeliberateAny, string>()
277+
const genericTypeMap = new Map<DeliberateAny, GenericTypeInfo>()
277278
export function captureGenericTypeInfo(target: DeliberateAny, t: string) {
278-
genericTypeMap.set(target, t)
279+
genericTypeMap.set(target, JSON.parse(t))
279280
return target
280281
}
281282

282-
export function getGenericTypeInfo(target: DeliberateAny): string | undefined {
283+
export function getGenericTypeInfo(target: DeliberateAny): GenericTypeInfo | undefined {
283284
return genericTypeMap.get(target)
284285
}

src/subcontexts/contract-context.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Account, Application, Asset, BaseContract, Bytes, bytes, Contract, Loca
22
import { getAbiMetadata } from '../abi-metadata'
33
import { BytesMap } from '../collections/custom-key-map'
44
import { lazyContext } from '../context-helpers/internal-context'
5+
import type { GenericTypeInfo } from '../encoders'
56
import { AccountCls } from '../impl/account'
67
import { ApplicationCls } from '../impl/application'
78
import { AssetCls } from '../impl/asset'
@@ -17,7 +18,7 @@ import {
1718
} from '../impl/transactions'
1819
import { getGenericTypeInfo } from '../runtime-helpers'
1920
import { DeliberateAny } from '../typescript-helpers'
20-
import { asUint64Cls, extractGenericTypeArgs } from '../util'
21+
import { asUint64Cls } from '../util'
2122

2223
interface IConstructor<T> {
2324
new (...args: DeliberateAny[]): T
@@ -31,10 +32,9 @@ interface States {
3132
totals: StateTotals
3233
}
3334

34-
const isUint64GenericType = (typeName: string | undefined) => {
35-
if (typeName === undefined) return false
36-
const genericTypes: string[] = extractGenericTypeArgs(typeName)
37-
return genericTypes.some((t) => t.toLocaleLowerCase() === 'uint64')
35+
const isUint64GenericType = (typeInfo: GenericTypeInfo | undefined) => {
36+
if (!typeInfo?.genericArgs?.length) return false
37+
return typeInfo.genericArgs.some((t) => t.name.toLocaleLowerCase() === 'uint64')
3838
}
3939

4040
const extractStates = (contract: BaseContract): States => {

src/subcontexts/ledger-context.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,12 @@ export class LedgerContext {
153153
}
154154
}
155155

156-
getBox<TValue>(app: Application, key: internal.primitives.StubBytesCompat): TValue {
156+
getBox(app: Application, key: internal.primitives.StubBytesCompat): Uint8Array {
157157
const appData = this.applicationDataMap.getOrFail(app.id)
158-
return appData.application.boxes.get(key) as TValue
158+
return appData.application.boxes.get(key) ?? new Uint8Array()
159159
}
160160

161-
setBox<TValue>(app: Application, key: internal.primitives.StubBytesCompat, value: TValue): void {
161+
setBox(app: Application, key: internal.primitives.StubBytesCompat, value: Uint8Array): void {
162162
const appData = this.applicationDataMap.getOrFail(app.id)
163163
appData.application.boxes.set(key, value)
164164
}

0 commit comments

Comments
 (0)