Skip to content

Commit 217a61c

Browse files
authored
Merge pull request #7 from algorandfoundation/feat-app-local
feat: implement AppLocal and add tests for it
2 parents 6c987a4 + 57fd3d1 commit 217a61c

16 files changed

+1086
-19
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.12",
66+
"@algorandfoundation/algorand-typescript": "^0.0.1-alpha.13",
6767
"@algorandfoundation/puya-ts": "^1.0.0-alpha.14",
6868
"algosdk": "^2.9.0",
6969
"elliptic": "^6.5.7",

src/impl/app-local.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Account, Application, Bytes, bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { lazyContext } from '../context-helpers/internal-context'
3+
import { asBytes } from '../util'
4+
import { getAccount } from './acct-params'
5+
import { getApp } from './app-params'
6+
7+
export const AppLocal: internal.opTypes.AppLocalType = {
8+
delete: function (a: Account | internal.primitives.StubUint64Compat, b: internal.primitives.StubBytesCompat): void {
9+
const app = lazyContext.activeApplication
10+
const account = getAccount(a)
11+
lazyContext.ledger.setLocalState(app, account, b, undefined)
12+
},
13+
getBytes: function (a: Account | internal.primitives.StubUint64Compat, b: internal.primitives.StubBytesCompat): bytes {
14+
const account = getAccount(a)
15+
return this.getExBytes(account, 0, asBytes(b))[0]
16+
},
17+
getUint64: function (a: Account | internal.primitives.StubUint64Compat, b: internal.primitives.StubBytesCompat): uint64 {
18+
const account = getAccount(a)
19+
return this.getExUint64(account, 0, asBytes(b))[0]
20+
},
21+
getExBytes: function (
22+
a: Account | internal.primitives.StubUint64Compat,
23+
b: Application | internal.primitives.StubUint64Compat,
24+
c: internal.primitives.StubBytesCompat,
25+
): readonly [bytes, boolean] {
26+
const app = getApp(b)
27+
const account = getAccount(a)
28+
if (app === undefined || account === undefined) {
29+
return [Bytes(), false]
30+
}
31+
const [state, exists] = lazyContext.ledger.getLocalState(app, account, c)
32+
if (!exists) {
33+
return [Bytes(), false]
34+
}
35+
return [state!.value as bytes, exists]
36+
},
37+
getExUint64: function (
38+
a: Account | internal.primitives.StubUint64Compat,
39+
b: Application | internal.primitives.StubUint64Compat,
40+
c: internal.primitives.StubBytesCompat,
41+
): readonly [uint64, boolean] {
42+
const app = getApp(b)
43+
const account = getAccount(a)
44+
if (app === undefined || account === undefined) {
45+
return [Uint64(0), false]
46+
}
47+
const [state, exists] = lazyContext.ledger.getLocalState(app, account, c)
48+
if (!exists) {
49+
return [Uint64(0), false]
50+
}
51+
return [state!.value as uint64, exists]
52+
},
53+
put: function (a: Account | internal.primitives.StubUint64Compat, b: internal.primitives.StubBytesCompat, c: uint64 | bytes): void {
54+
const app = lazyContext.activeApplication
55+
const account = getAccount(a)
56+
lazyContext.ledger.setLocalState(app, account, b, c)
57+
},
58+
}

src/impl/application.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import { Account, Application, Bytes, bytes, uint64 } from '@algorandfoundation/algorand-typescript'
1+
import { Account, Application, Bytes, bytes, LocalState, uint64 } from '@algorandfoundation/algorand-typescript'
22
import algosdk from 'algosdk'
33
import { BytesMap } from '../collections/custom-key-map'
44
import { ALWAYS_APPROVE_TEAL_PROGRAM } from '../constants'
55
import { lazyContext } from '../context-helpers/internal-context'
66
import { Mutable } from '../typescript-helpers'
77
import { asBigInt, asUint64 } from '../util'
88
import { Uint64BackedCls } from './base'
9-
import { GlobalStateCls, LocalStateMapCls } from './state'
9+
import { GlobalStateCls } from './state'
1010

1111
export class ApplicationData {
1212
application: Mutable<Omit<Application, 'id' | 'address'>> & {
1313
appLogs: bytes[]
1414
globalStates: BytesMap<GlobalStateCls<unknown>>
15-
localStates: BytesMap<LocalStateMapCls<unknown>>
15+
localStates: BytesMap<LocalState<unknown>>
1616
}
1717
isCreating: boolean = false
1818

src/impl/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { AcctParams, balance, minBalance } from './acct-params'
22
export { AppGlobal } from './app-global'
3+
export { AppLocal } from './app-local'
34
export { AppParams } from './app-params'
45
export { AssetHolding } from './asset-holding'
56
export { AssetParams } from './asset-params'

src/impl/state.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
LocalStateForAccount,
99
Uint64,
1010
} from '@algorandfoundation/algorand-typescript'
11-
import { asBytes, asBytesCls } from '../util'
11+
import { AccountMap } from '../collections/custom-key-map'
12+
import { asBytes } from '../util'
1213

1314
export class GlobalStateCls<ValueType> {
1415
private readonly _type: string = GlobalStateCls.name
@@ -75,14 +76,13 @@ export class LocalStateCls<ValueType> {
7576
}
7677

7778
export class LocalStateMapCls<ValueType> {
78-
#value = new Map<string, LocalStateCls<ValueType>>()
79+
#value = new AccountMap<LocalStateCls<ValueType>>()
7980

8081
getValue(account: Account): LocalStateCls<ValueType> {
81-
const accountString = asBytesCls(account.bytes).valueOf()
82-
if (!this.#value.has(accountString)) {
83-
this.#value.set(accountString, new LocalStateCls<ValueType>())
82+
if (!this.#value.has(account)) {
83+
this.#value.set(account, new LocalStateCls<ValueType>())
8484
}
85-
return this.#value.get(accountString)!
85+
return this.#value.getOrFail(account)!
8686
}
8787
}
8888

src/subcontexts/contract-context.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { Account, Application, Asset, BaseContract, Bytes, bytes, Contract } from '@algorandfoundation/algorand-typescript'
1+
import { Account, Application, Asset, BaseContract, Bytes, bytes, Contract, LocalState } from '@algorandfoundation/algorand-typescript'
22
import { getAbiMetadata } from '../abi-metadata'
33
import { BytesMap } from '../collections/custom-key-map'
44
import { lazyContext } from '../context-helpers/internal-context'
55
import { AccountCls } from '../impl/account'
66
import { ApplicationCls } from '../impl/application'
77
import { AssetCls } from '../impl/asset'
8-
import { GlobalStateCls, LocalStateMapCls } from '../impl/state'
8+
import { GlobalStateCls } from '../impl/state'
99
import {
1010
ApplicationTransaction,
1111
AssetConfigTransaction,
@@ -27,7 +27,7 @@ type StateTotals = Pick<Application, 'globalNumBytes' | 'globalNumUint' | 'local
2727

2828
interface States {
2929
globalStates: BytesMap<GlobalStateCls<unknown>>
30-
localStates: BytesMap<LocalStateMapCls<unknown>>
30+
localStates: BytesMap<LocalState<unknown>>
3131
totals: StateTotals
3232
}
3333

@@ -41,7 +41,7 @@ const extractStates = (contract: BaseContract): States => {
4141
const stateTotals = { globalNumBytes: 0, globalNumUint: 0, localNumBytes: 0, localNumUint: 0 }
4242
const states = {
4343
globalStates: new BytesMap<GlobalStateCls<unknown>>(),
44-
localStates: new BytesMap<LocalStateMapCls<unknown>>(),
44+
localStates: new BytesMap<LocalState<unknown>>(),
4545
totals: stateTotals,
4646
}
4747
Object.entries(contract).forEach(([key, value]) => {

src/subcontexts/ledger-context.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Account, Application, Asset, BaseContract, internal } from '@algorandfoundation/algorand-typescript'
1+
import { Account, Application, Asset, BaseContract, internal, LocalStateForAccount } from '@algorandfoundation/algorand-typescript'
22
import { AccountMap, Uint64Map } from '../collections/custom-key-map'
33
import { MAX_UINT64 } from '../constants'
44
import { AccountData, AssetHolding } from '../impl/account'
@@ -123,4 +123,33 @@ export class LedgerContext {
123123
globalState.value = asMaybeUint64Cls(value) ?? asMaybeBytesCls(value)
124124
}
125125
}
126+
127+
getLocalState(
128+
app: Application,
129+
account: Account,
130+
key: internal.primitives.StubBytesCompat,
131+
): [LocalStateForAccount<unknown>, true] | [undefined, false] {
132+
const appData = this.applicationDataMap.get(app.id)
133+
if (!appData?.application.localStates.has(key)) {
134+
return [undefined, false]
135+
}
136+
const localState = appData.application.localStates.getOrFail(key)
137+
return [localState(account), true]
138+
}
139+
140+
setLocalState(
141+
app: Application,
142+
account: Account,
143+
key: internal.primitives.StubBytesCompat,
144+
value: internal.primitives.StubUint64Compat | internal.primitives.StubBytesCompat | undefined,
145+
): void {
146+
const appData = this.applicationDataMap.getOrFail(app.id)
147+
const localState = appData.application.localStates.getOrFail(key)
148+
const accountLocalState = localState(account)
149+
if (value === undefined) {
150+
accountLocalState.delete()
151+
} else {
152+
accountLocalState.value = asMaybeUint64Cls(value) ?? asMaybeBytesCls(value)
153+
}
154+
}
126155
}

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Global,
1111
GlobalState,
1212
itxn,
13+
LocalState,
1314
op,
1415
TransactionType,
1516
Txn,
@@ -405,6 +406,78 @@ export class StateAppGlobalContract extends arc4.Contract {
405406
}
406407
}
407408

409+
export class StateAppLocalExContract extends arc4.Contract {
410+
localUint64 = LocalState<uint64>({ key: 'local_uint64' })
411+
localBytes = LocalState<bytes>({ key: 'local_bytes' })
412+
413+
// TODO: uncomment when arc4 types are ready
414+
// localArc4Bytes = LocalState<arc4.DynamicBytes>({ key: "local_arc4_bytes" })
415+
416+
@arc4.abimethod({ allowActions: ['OptIn'] })
417+
optIn(): void {
418+
this.localBytes(Global.creatorAddress).value = Bytes('dummy_bytes_from_external_contract')
419+
this.localUint64(Global.creatorAddress).value = Uint64(99)
420+
// TODO: uncomment when arc4 types are ready
421+
// this.localArc4Bytes(Global.creatorAddress).value = arc4.DynamicBytes("dummy_arc4_bytes")
422+
}
423+
}
424+
425+
export class StateAppLocalContract extends arc4.Contract {
426+
localUint64 = LocalState<uint64>({ key: 'local_uint64' })
427+
localBytes = LocalState<bytes>({ key: 'local_bytes' })
428+
429+
@arc4.abimethod({ allowActions: ['OptIn'] })
430+
opt_in() {
431+
this.localBytes(Global.creatorAddress).value = Bytes('dummy_bytes')
432+
this.localUint64(Global.creatorAddress).value = Uint64(999)
433+
}
434+
435+
@arc4.abimethod()
436+
verify_get_bytes(a: Account, b: bytes): bytes {
437+
const value = op.AppLocal.getBytes(a, b)
438+
return value
439+
}
440+
441+
@arc4.abimethod()
442+
verify_get_uint64(a: Account, b: bytes): uint64 {
443+
const value = op.AppLocal.getUint64(a, b)
444+
return value
445+
}
446+
447+
@arc4.abimethod()
448+
verify_get_ex_bytes(a: Account, b: Application, c: bytes): bytes {
449+
const [value, _val] = op.AppLocal.getExBytes(a, b, c)
450+
return value
451+
}
452+
453+
@arc4.abimethod()
454+
verify_get_ex_uint64(a: Account, b: Application, c: bytes): uint64 {
455+
const [value, _val] = op.AppLocal.getExUint64(a, b, c)
456+
return value
457+
}
458+
459+
@arc4.abimethod()
460+
verify_delete(a: Account, b: bytes): void {
461+
op.AppLocal.delete(a, b)
462+
}
463+
464+
@arc4.abimethod()
465+
verify_exists(a: Account, b: bytes): boolean {
466+
const [_value, exists] = op.AppLocal.getExUint64(a, 0, b)
467+
return exists
468+
}
469+
470+
@arc4.abimethod()
471+
verify_put_uint64(a: Account, b: bytes, c: uint64): void {
472+
op.AppLocal.put(a, b, c)
473+
}
474+
475+
@arc4.abimethod()
476+
verify_put_bytes(a: Account, b: bytes, c: bytes): void {
477+
op.AppLocal.put(a, b, c)
478+
}
479+
}
480+
408481
export class ITxnOpsContract extends arc4.Contract {
409482
@arc4.abimethod()
410483
public verify_itxn_ops() {

0 commit comments

Comments
 (0)