Skip to content

Commit 613ba33

Browse files
authored
Merge pull request #8 from algorandfoundation/feat-box
feat: implement Box op code and Box functions
2 parents 217a61c + c8abc46 commit 613ba33

25 files changed

+1452
-63
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
},
6464
"dependencies": {
6565
"@algorandfoundation/algokit-utils": "^6.2.1",
66-
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.13",
66+
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.15",
6767
"@algorandfoundation/puya-ts": "^1.0.0-alpha.14",
6868
"algosdk": "^2.9.0",
6969
"elliptic": "^6.5.7",

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const MAX_UINT64 = 2n ** 64n - 1n
77
export const MAX_UINT512 = 2n ** 512n - 1n
88
export const MAX_BYTES_SIZE = 4096
99
export const MAX_ITEMS_IN_LOG = 32
10+
export const MAX_BOX_SIZE = 32768
1011
export const BITS_IN_BYTE = 8
1112
export const DEFAULT_ACCOUNT_MIN_BALANCE = 100_000
1213
export const DEFAULT_MAX_TXN_LIFE = 1_000

src/encoders.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 TypeInfo {
8+
name: string
9+
genericArgs?: TypeInfo[] | Record<string, TypeInfo>
10+
}
11+
12+
type fromBytes<T> = (val: Uint8Array, typeInfo: TypeInfo) => T
13+
14+
const booleanFromBytes: fromBytes<boolean> = (val) => {
15+
return internal.encodingUtil.uint8ArrayToBigInt(val) > 0n
16+
}
17+
18+
const bigUintFromBytes: fromBytes<biguint> = (val) => {
19+
return BigUint(internal.encodingUtil.uint8ArrayToBigInt(val))
20+
}
21+
22+
const bytesFromBytes: fromBytes<bytes> = (val) => {
23+
return Bytes(val)
24+
}
25+
26+
const stringFromBytes: fromBytes<string> = (val) => {
27+
return Bytes(val).toString()
28+
}
29+
30+
const uint64FromBytes: fromBytes<uint64> = (val) => {
31+
return Uint64(internal.encodingUtil.uint8ArrayToBigInt(val))
32+
}
33+
34+
const onCompletionFromBytes: fromBytes<OnCompleteAction> = (val) => {
35+
return Uint64(internal.encodingUtil.uint8ArrayToBigInt(val)) as OnCompleteAction
36+
}
37+
38+
const transactionTypeFromBytes: fromBytes<TransactionType> = (val) => {
39+
return Uint64(internal.encodingUtil.uint8ArrayToBigInt(val)) as TransactionType
40+
}
41+
42+
export const encoders = {
43+
account: AccountCls.fromBytes,
44+
application: ApplicationCls.fromBytes,
45+
asset: AssetCls.fromBytes,
46+
'bool(ean)?': booleanFromBytes,
47+
biguint: bigUintFromBytes,
48+
bytes: bytesFromBytes,
49+
string: stringFromBytes,
50+
uint64: uint64FromBytes,
51+
OnCompleteAction: onCompletionFromBytes,
52+
TransactionType: transactionTypeFromBytes,
53+
// 'Tuple<*>': tupleFromBytes,
54+
}
55+
56+
export const getEncoder = <T>(typeInfo: TypeInfo): fromBytes<T> => {
57+
const encoder = Object.entries(encoders).find(([k, _]) => new RegExp(`^${k}$`, 'i').test(typeInfo.name))?.[1]
58+
if (!encoder) {
59+
throw new Error(`No encoder found for type ${typeInfo.name}`)
60+
}
61+
return encoder as fromBytes<T>
62+
}

src/impl/application.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class ApplicationData {
1313
appLogs: bytes[]
1414
globalStates: BytesMap<GlobalStateCls<unknown>>
1515
localStates: BytesMap<LocalState<unknown>>
16+
boxes: BytesMap<Uint8Array>
1617
}
1718
isCreating: boolean = false
1819

@@ -33,6 +34,7 @@ export class ApplicationData {
3334
appLogs: [],
3435
globalStates: new BytesMap(),
3536
localStates: new BytesMap(),
37+
boxes: new BytesMap(),
3638
}
3739
}
3840
}

src/impl/base.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
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 { TypeInfo } 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?: TypeInfo) {
1013
this.#value = value
14+
// this.#typeInfo = typeInfo
15+
}
16+
17+
static fromBytes<T extends BytesBackedCls>(this: { new (v: bytes, typeInfo?: TypeInfo): T }, value: Uint8Array, typeInfo?: TypeInfo) {
18+
return new this(Bytes(value), typeInfo)
1119
}
1220
}
1321

@@ -21,4 +29,9 @@ export abstract class Uint64BackedCls {
2129
constructor(value: uint64) {
2230
this.#value = value
2331
}
32+
33+
static fromBytes<T extends Uint64BackedCls>(this: { new (v: uint64): T }, value: Uint8Array) {
34+
const uint64Value = Uint64(encodingUtil.uint8ArrayToBigInt(value))
35+
return new this(uint64Value)
36+
}
2437
}

src/impl/box.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { MAX_BOX_SIZE } from '../constants'
3+
import { lazyContext } from '../context-helpers/internal-context'
4+
import { asBytes, asBytesCls, asNumber, asUint8Array, conactUint8Arrays, toBytes } from '../util'
5+
6+
export const Box: internal.opTypes.BoxType = {
7+
create(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubUint64Compat): boolean {
8+
const name = asBytes(a)
9+
const size = asNumber(b)
10+
if (name.length === 0 || size > MAX_BOX_SIZE) {
11+
throw new internal.errors.InternalError('Invalid box name or size')
12+
}
13+
const app = lazyContext.activeApplication
14+
if (lazyContext.ledger.boxExists(app, name)) {
15+
return false
16+
}
17+
lazyContext.ledger.setBox(app, name, new Uint8Array(size))
18+
return true
19+
},
20+
delete(a: internal.primitives.StubBytesCompat): boolean {
21+
const name = asBytes(a)
22+
const app = lazyContext.activeApplication
23+
if (!lazyContext.ledger.boxExists(app, name)) {
24+
return false
25+
}
26+
lazyContext.ledger.deleteBox(app, name)
27+
return true
28+
},
29+
extract(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubUint64Compat, c: internal.primitives.StubUint64Compat): bytes {
30+
const name = asBytes(a)
31+
const start = asNumber(b)
32+
const length = asNumber(c)
33+
const app = lazyContext.activeApplication
34+
if (!lazyContext.ledger.boxExists(app, name)) {
35+
throw new internal.errors.InternalError('Box does not exist')
36+
}
37+
const boxContent = lazyContext.ledger.getBox(app, name)
38+
return toBytes(boxContent.slice(start, start + length))
39+
},
40+
get(a: internal.primitives.StubBytesCompat): readonly [bytes, boolean] {
41+
const name = asBytes(a)
42+
const app = lazyContext.activeApplication
43+
const boxContent = lazyContext.ledger.getBox(app, name)
44+
return [toBytes(boxContent), lazyContext.ledger.boxExists(app, name)]
45+
},
46+
length(a: internal.primitives.StubBytesCompat): readonly [uint64, boolean] {
47+
const name = asBytes(a)
48+
const app = lazyContext.activeApplication
49+
const boxContent = lazyContext.ledger.getBox(app, name)
50+
const exists = lazyContext.ledger.boxExists(app, name)
51+
return [boxContent.length, exists]
52+
},
53+
put(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubBytesCompat): void {
54+
const name = asBytes(a)
55+
const app = lazyContext.activeApplication
56+
const newContent = asBytesCls(b)
57+
if (lazyContext.ledger.boxExists(app, name)) {
58+
const boxContent = lazyContext.ledger.getBox(app, name)
59+
const length = boxContent.length
60+
if (asNumber(length) !== asNumber(newContent.length)) {
61+
throw new internal.errors.InternalError('New content length does not match existing box length')
62+
}
63+
}
64+
lazyContext.ledger.setBox(app, name, newContent.asUint8Array())
65+
},
66+
replace(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubUint64Compat, c: internal.primitives.StubBytesCompat): void {
67+
const name = asBytes(a)
68+
const start = asNumber(b)
69+
const newContent = asUint8Array(c)
70+
const app = lazyContext.activeApplication
71+
if (!lazyContext.ledger.boxExists(app, name)) {
72+
throw new internal.errors.InternalError('Box does not exist')
73+
}
74+
const boxContent = lazyContext.ledger.getBox(app, name)
75+
if (start + newContent.length > boxContent.length) {
76+
throw new internal.errors.InternalError('Replacement content exceeds box size')
77+
}
78+
const updatedContent = conactUint8Arrays(boxContent.slice(0, start), newContent, boxContent.slice(start + newContent.length))
79+
lazyContext.ledger.setBox(app, name, updatedContent)
80+
},
81+
resize(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubUint64Compat): void {
82+
const name = asBytes(a)
83+
const newSize = asNumber(b)
84+
const app = lazyContext.activeApplication
85+
if (!lazyContext.ledger.boxExists(app, name)) {
86+
throw new internal.errors.InternalError('Box does not exist')
87+
}
88+
const boxContent = lazyContext.ledger.getBox(app, name)
89+
const size = boxContent.length
90+
let updatedContent
91+
if (newSize > size) {
92+
updatedContent = conactUint8Arrays(boxContent, new Uint8Array(newSize - size))
93+
} else {
94+
updatedContent = boxContent.slice(0, newSize)
95+
}
96+
lazyContext.ledger.setBox(app, name, updatedContent)
97+
},
98+
splice(
99+
a: internal.primitives.StubBytesCompat,
100+
b: internal.primitives.StubUint64Compat,
101+
c: internal.primitives.StubUint64Compat,
102+
d: internal.primitives.StubBytesCompat,
103+
): void {
104+
const name = asBytes(a)
105+
const start = asNumber(b)
106+
const length = asNumber(c)
107+
const newContent = asUint8Array(d)
108+
const app = lazyContext.activeApplication
109+
if (!lazyContext.ledger.boxExists(app, name)) {
110+
throw new internal.errors.InternalError('Box does not exist')
111+
}
112+
const boxContent = lazyContext.ledger.getBox(app, name)
113+
const size = boxContent.length
114+
if (start > size) {
115+
throw new internal.errors.InternalError('Start index exceeds box size')
116+
}
117+
const end = Math.min(start + length, size)
118+
let updatedContent = conactUint8Arrays(boxContent.slice(0, start), newContent, boxContent.slice(end))
119+
// Adjust the size if necessary
120+
if (updatedContent.length > size) {
121+
updatedContent = updatedContent.slice(0, size)
122+
} else if (updatedContent.length < size) {
123+
updatedContent = conactUint8Arrays(updatedContent, new Uint8Array(size - asNumber(updatedContent.length)))
124+
}
125+
lazyContext.ledger.setBox(app, name, updatedContent)
126+
},
127+
}

src/impl/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { AppLocal } from './app-local'
44
export { AppParams } from './app-params'
55
export { AssetHolding } from './asset-holding'
66
export { AssetParams } from './asset-params'
7+
export { Box } from './box'
78
export * from './crypto'
89
export { Global } from './global'
910
export { GTxn } from './gtxn'

src/impl/pure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const bzero = (a: internal.primitives.StubUint64Compat): bytes => {
5252
if (size > MAX_BYTES_SIZE) {
5353
internal.errors.avmError('bzero attempted to create a too large string')
5454
}
55-
return Bytes(new Uint8Array(Array(Number(size)).fill(0x00)))
55+
return Bytes(new Uint8Array(Number(size)))
5656
}
5757

5858
export const concat = (a: internal.primitives.StubBytesCompat, b: internal.primitives.StubBytesCompat): bytes => {

0 commit comments

Comments
 (0)