Skip to content

Commit 9ce9a66

Browse files
authored
Merge pull request #66 from algorandfoundation/fix/box-mutation
fix: allow mutation of values stored in boxes, automatically register app address with context
2 parents 50989f8 + 9095196 commit 9ce9a66

File tree

7 files changed

+161
-11
lines changed

7 files changed

+161
-11
lines changed

package-lock.json

+58-5
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
@@ -67,7 +67,7 @@
6767
},
6868
"dependencies": {
6969
"@algorandfoundation/algorand-typescript": "^1.0.0-beta.25",
70-
"@algorandfoundation/puya-ts": "^1.0.0-beta.43",
70+
"@algorandfoundation/puya-ts": "^1.0.0-beta.46",
7171
"elliptic": "^6.5.7",
7272
"js-sha256": "^0.11.0",
7373
"js-sha3": "^0.9.3",

src/impl/reference.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from '../constants'
2323
import { lazyContext } from '../context-helpers/internal-context'
2424
import { AvmError, InternalError } from '../errors'
25-
import type { Mutable } from '../typescript-helpers'
25+
import type { DeliberateAny, Mutable } from '../typescript-helpers'
2626
import { asBigInt, asBytes, asUint64, asUint64Cls, asUint8Array, conactUint8Arrays } from '../util'
2727
import { BytesBackedCls, Uint64BackedCls } from './base'
2828
import type { StubUint64Compat } from './primitives'
@@ -136,6 +136,7 @@ export class ApplicationData {
136136
localStates: BytesMap<LocalState<unknown>>
137137
localStateMaps: BytesMap<AccountMap<LocalStateCls<unknown>>>
138138
boxes: BytesMap<Uint8Array>
139+
materialisedBoxes: BytesMap<DeliberateAny>
139140
}
140141

141142
isCreating: boolean = false
@@ -155,6 +156,7 @@ export class ApplicationData {
155156
localStates: new BytesMap(),
156157
localStateMaps: new BytesMap(),
157158
boxes: new BytesMap(),
159+
materialisedBoxes: new BytesMap(),
158160
}
159161
}
160162
}
@@ -200,7 +202,11 @@ export class ApplicationCls extends Uint64BackedCls implements ApplicationType {
200202
return this.data.application.creator
201203
}
202204
get address(): AccountType {
203-
return getApplicationAddress(this.id)
205+
const result = getApplicationAddress(this.id)
206+
if (!lazyContext.ledger.accountDataMap.has(result)) {
207+
lazyContext.any.account({ address: result.bytes })
208+
}
209+
return result
204210
}
205211
}
206212

src/impl/state.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,18 @@ export class BoxCls<TValue> {
155155
if (!this.exists) {
156156
throw new InternalError('Box has not been created')
157157
}
158-
159-
return this.fromBytes(lazyContext.ledger.getBox(this.#app, this.key))
158+
let materialised = lazyContext.ledger.getMaterialisedBox<TValue>(this.#app, this.key)
159+
if (materialised !== undefined) {
160+
return materialised
161+
}
162+
const original = lazyContext.ledger.getBox(this.#app, this.key)
163+
materialised = this.fromBytes(original)
164+
lazyContext.ledger.setMatrialisedBox(this.#app, this.key, materialised)
165+
return materialised
160166
}
161167
set value(v: TValue) {
162168
lazyContext.ledger.setBox(this.#app, this.key, asUint8Array(toBytes(v)))
169+
lazyContext.ledger.setMatrialisedBox(this.#app, this.key, v)
163170
}
164171

165172
get hasKey(): boolean {

src/subcontexts/ledger-context.ts

+34
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
} from '@algorandfoundation/algorand-typescript'
1010
import { AccountMap, Uint64Map } from '../collections/custom-key-map'
1111
import { MAX_UINT64 } from '../constants'
12+
import { toBytes } from '../encoders'
1213
import { InternalError } from '../errors'
1314
import { BlockData } from '../impl/block'
1415
import { GlobalData } from '../impl/global'
@@ -340,9 +341,26 @@ export class LedgerContext {
340341
getBox(app: ApplicationType | BaseContract, key: StubBytesCompat): Uint8Array {
341342
const appId = this.getAppId(app)
342343
const appData = this.applicationDataMap.getOrFail(appId)
344+
const materialised = appData.application.materialisedBoxes.get(key)
345+
if (materialised !== undefined) {
346+
return asUint8Array(toBytes(materialised))
347+
}
343348
return appData.application.boxes.get(key) ?? new Uint8Array()
344349
}
345350

351+
/**
352+
* Retrieves a materialised box for an application by key.
353+
* @internal
354+
* @param app - The application.
355+
* @param key - The key.
356+
* @returns The materialised box data if exists or undefined.
357+
*/
358+
getMaterialisedBox<T>(app: ApplicationType | BaseContract, key: StubBytesCompat): T | undefined {
359+
const appId = this.getAppId(app)
360+
const appData = this.applicationDataMap.getOrFail(appId)
361+
return appData.application.materialisedBoxes.get(key) as T | undefined
362+
}
363+
346364
/**
347365
* Sets a box for an application by key.
348366
* @param app - The application.
@@ -354,6 +372,21 @@ export class LedgerContext {
354372
const appData = this.applicationDataMap.getOrFail(appId)
355373
const uint8ArrayValue = value instanceof Uint8Array ? value : asUint8Array(value)
356374
appData.application.boxes.set(key, uint8ArrayValue)
375+
appData.application.materialisedBoxes.set(key, undefined)
376+
}
377+
378+
/**
379+
380+
* Cache the materialised box for an application by key.
381+
* @internal
382+
* @param app - The application.
383+
* @param key - The key.
384+
* @param value - The box data.
385+
*/
386+
setMatrialisedBox<TValue>(app: ApplicationType | BaseContract, key: StubBytesCompat, value: TValue | undefined): void {
387+
const appId = this.getAppId(app)
388+
const appData = this.applicationDataMap.getOrFail(appId)
389+
appData.application.materialisedBoxes.set(key, value)
357390
}
358391

359392
/**
@@ -365,6 +398,7 @@ export class LedgerContext {
365398
deleteBox(app: ApplicationType | BaseContract, key: StubBytesCompat): boolean {
366399
const appId = this.getAppId(app)
367400
const appData = this.applicationDataMap.getOrFail(appId)
401+
appData.application.materialisedBoxes.delete(key)
368402
return appData.application.boxes.delete(key)
369403
}
370404

tests/references/box-map.spec.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { biguint, bytes, uint64 } from '@algorandfoundation/algorand-typesc
22
import { BigUint, BoxMap, Bytes, op, Uint64 } from '@algorandfoundation/algorand-typescript'
33
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
44
import { ARC4Encoded, DynamicArray, interpretAsArc4, Str, UintN64 } from '@algorandfoundation/algorand-typescript/arc4'
5-
import { afterEach, describe, expect, test } from 'vitest'
5+
import { afterEach, describe, expect, it, test } from 'vitest'
66
import { MAX_UINT64 } from '../../src/constants'
77
import { toBytes } from '../../src/encoders'
88
import { asBytes } from '../../src/util'
@@ -204,4 +204,30 @@ describe('BoxMap', () => {
204204
}
205205
})
206206
})
207+
208+
it('can maintain the mutations to the array box value', () => {
209+
ctx.txn.createScope([ctx.any.txn.applicationCall()]).execute(() => {
210+
const boxMap = BoxMap<Str, DynamicArray<UintN64>>({ keyPrefix })
211+
const key = new Str('jkl')
212+
const value = new DynamicArray(new UintN64(100), new UintN64(200))
213+
boxMap(key).value = value
214+
expect(boxMap(key).value.length).toEqual(2)
215+
expect(boxMap(key).value.at(-1).native).toEqual(200)
216+
217+
// newly pushed value should be retained
218+
boxMap(key).value.push(new UintN64(300))
219+
expect(boxMap(key).value.length).toEqual(3)
220+
expect(boxMap(key).value.at(-1).native).toEqual(300)
221+
222+
// setting bytes value through op should be reflected in the box value.
223+
const copy = boxMap(key).value.copy()
224+
copy[2] = new UintN64(400)
225+
expect(boxMap(key).value.at(-1).native).toEqual(300)
226+
227+
const fullKey = keyPrefix.concat(toBytes(key))
228+
op.Box.put(fullKey, toBytes(copy))
229+
expect(boxMap(key).value.length).toEqual(3)
230+
expect(boxMap(key).value.at(-1).native).toEqual(400)
231+
})
232+
})
207233
})

tests/references/box.spec.ts

+24
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,28 @@ describe('Box', () => {
205205
}
206206
})
207207
})
208+
209+
it('can maintain the mutations to the box value', () => {
210+
ctx.txn.createScope([ctx.any.txn.applicationCall()]).execute(() => {
211+
const box = Box<DynamicArray<UintN64>>({ key })
212+
const value = new DynamicArray(new UintN64(100), new UintN64(200))
213+
box.value = value
214+
expect(box.value.length).toEqual(2)
215+
expect(box.value.at(-1).native).toEqual(200)
216+
217+
// newly pushed value should be retained
218+
box.value.push(new UintN64(300))
219+
expect(box.value.length).toEqual(3)
220+
expect(box.value.at(-1).native).toEqual(300)
221+
222+
// setting bytes value through op should be reflected in the box value.
223+
const copy = box.value.copy()
224+
copy[2] = new UintN64(400)
225+
expect(box.value.at(-1).native).toEqual(300)
226+
227+
op.Box.put(key, toBytes(copy))
228+
expect(box.value.length).toEqual(3)
229+
expect(box.value.at(-1).native).toEqual(400)
230+
})
231+
})
208232
})

0 commit comments

Comments
 (0)