Skip to content

Commit cd78814

Browse files
committed
feat: implement AppGlobal and add tests for it
- Contract proxy returns original value without wrapping for all entries which are not ABI methods in an arc4 contract - allow invocation of contract methods which return void
1 parent c2b789d commit cd78814

20 files changed

+937
-34
lines changed

.editorconfig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[*]
2+
charset = utf-8
3+
insert_final_newline = true
4+
end_of_line = lf
5+
indent_style = space
6+
indent_size = 2
7+
tab_width = 2
8+
max_line_length = 140
9+
trim_trailing_whitespace = true

.gitattributes

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Set the repository to show as TypeScript rather than JS in GitHub
2+
*.js linguist-detectable=false
3+
4+
# Treat text as lf
5+
* text=auto
6+
* eol=lf

.vscode/settings.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"editor.formatOnSave": true,
3+
"editor.defaultFormatter": "esbenp.prettier-vscode",
4+
"editor.codeActionsOnSave": {
5+
"source.fixAll.eslint": "explicit",
6+
"source.organizeImports": "explicit"
7+
},
8+
"eslint.validate": ["typescript"],
9+
"eslint.options": {
10+
"extensions": [".ts"]
11+
},
12+
"typescript.preferences.quoteStyle": "single",
13+
"yaml.schemas": {
14+
"https://json.schemastore.org/github-workflow.json": "/.github/workflows/*.yml",
15+
"https://json.schemastore.org/github-action.json": "/.github/actions/**/*.yml"
16+
},
17+
"files.associations": {
18+
"*.mdx": "markdown"
19+
}
20+
}

src/collections/custom-key-map.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Account, internal } from '@algorandfoundation/algorand-typescript'
2-
import { encodingUtil } from '@algorandfoundation/puya-ts'
1+
import { Account, bytes } from '@algorandfoundation/algorand-typescript'
32
import { DeliberateAny } from '../typescript-helpers'
3+
import { asBytesCls } from '../util'
44

55
type Primitive = number | bigint | string | boolean
66
export abstract class CustomKeyMap<TKey, TValue> implements Map<TKey, TValue> {
@@ -62,6 +62,12 @@ export class AccountMap<TValue> extends CustomKeyMap<Account, TValue> {
6262
}
6363

6464
private static getAddressStrFromAccount = (acc: Account) => {
65-
return encodingUtil.uint8ArrayToHex(internal.primitives.BytesCls.fromCompat(acc.bytes).asUint8Array())
65+
return asBytesCls(acc.bytes).valueOf()
66+
}
67+
}
68+
69+
export class BytesMap<TValue> extends CustomKeyMap<bytes, TValue> {
70+
constructor() {
71+
super((bytes) => asBytesCls(bytes).valueOf())
6672
}
6773
}

src/impl/app-global.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { 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 { getApp } from './app-params'
5+
6+
export const AppGlobal: internal.opTypes.AppGlobalType = {
7+
delete: function (a: internal.primitives.StubBytesCompat): void {
8+
lazyContext.ledger.setGlobalState(lazyContext.activeApplication, a, undefined)
9+
},
10+
getBytes: function (a: internal.primitives.StubBytesCompat): bytes {
11+
return this.getExBytes(0, asBytes(a))[0]
12+
},
13+
getUint64: function (a: internal.primitives.StubBytesCompat): uint64 {
14+
return this.getExUint64(0, asBytes(a))[0]
15+
},
16+
getExBytes: function (
17+
a: Application | internal.primitives.StubUint64Compat,
18+
b: internal.primitives.StubBytesCompat,
19+
): readonly [bytes, boolean] {
20+
const app = getApp(a)
21+
if (app === undefined) {
22+
return [Bytes(), false]
23+
}
24+
const [state, exists] = lazyContext.ledger.getGlobalState(app, b)
25+
if (!exists) {
26+
return [Bytes(), false]
27+
}
28+
return [state!.value as bytes, exists]
29+
},
30+
getExUint64: function (
31+
a: Application | internal.primitives.StubUint64Compat,
32+
b: internal.primitives.StubBytesCompat,
33+
): readonly [uint64, boolean] {
34+
const app = getApp(a)
35+
if (app === undefined) {
36+
return [Uint64(0), false]
37+
}
38+
const [state, exists] = lazyContext.ledger.getGlobalState(app, b)
39+
if (!exists) {
40+
return [Uint64(0), false]
41+
}
42+
return [state!.value as uint64, exists]
43+
},
44+
put: function (
45+
a: internal.primitives.StubBytesCompat,
46+
b: internal.primitives.StubUint64Compat | internal.primitives.StubBytesCompat,
47+
): void {
48+
lazyContext.ledger.setGlobalState(lazyContext.activeApplication, a, b)
49+
},
50+
}

src/impl/application.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import { Account, Application, Bytes, bytes, uint64 } from '@algorandfoundation/algorand-typescript'
1+
import { Account, Application, Bytes, bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
22
import algosdk from 'algosdk'
3+
import { BytesMap } from '../collections/custom-key-map'
34
import { ALWAYS_APPROVE_TEAL_PROGRAM } from '../constants'
45
import { lazyContext } from '../context-helpers/internal-context'
56
import { Mutable } from '../typescript-helpers'
67
import { asBigInt, asUint64 } from '../util'
78
import { Uint64BackedCls } from './base'
89

910
export class ApplicationData {
10-
application: Mutable<Omit<Application, 'id' | 'address'>> & { appLogs: bytes[] }
11+
application: Mutable<Omit<Application, 'id' | 'address'>> & {
12+
appLogs: bytes[]
13+
globalStates: BytesMap<internal.state.GlobalStateCls<unknown>>
14+
localStates: BytesMap<internal.state.LocalStateMapCls<unknown>>
15+
}
1116
isCreating: boolean = false
1217

1318
get appLogs() {
@@ -25,6 +30,8 @@ export class ApplicationData {
2530
extraProgramPages: 0,
2631
creator: lazyContext.defaultSender,
2732
appLogs: [],
33+
globalStates: new BytesMap(),
34+
localStates: new BytesMap(),
2835
}
2936
}
3037
}

src/impl/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
export { AcctParams, balance, minBalance } from './acct-params'
2+
export { AppGlobal } from './app-global'
23
export { AppParams } from './app-params'
34
export { AssetHolding } from './asset-holding'
45
export { AssetParams } from './asset-params'
56
export * from './crypto'
67
export { Global } from './global'
78
export { GTxn } from './gtxn'
8-
export * from './pure'
9-
export { Txn, gaid } from './txn'
109
export { GITxn, ITxn, ITxnCreate } from './itxn'
10+
export * from './pure'
1111
export { Scratch, gloadBytes, gloadUint64 } from './scratch'
1212
export { Block } from './block'
13+
export { Txn, gaid } from './txn'

src/subcontexts/contract-context.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Account, Application, Asset, BaseContract, Bytes, bytes, Contract, internal } from '@algorandfoundation/algorand-typescript'
22
import { getAbiMetadata } from '../abi-metadata'
3+
import { BytesMap } from '../collections/custom-key-map'
34
import { lazyContext } from '../context-helpers/internal-context'
45
import { AccountCls } from '../impl/account'
56
import { ApplicationCls } from '../impl/application'
@@ -15,7 +16,7 @@ import {
1516
} from '../impl/transactions'
1617
import { getGenericTypeInfo } from '../runtime-helpers'
1718
import { DeliberateAny } from '../typescript-helpers'
18-
import { extractGenericTypeArgs } from '../util'
19+
import { asUint64Cls, extractGenericTypeArgs } from '../util'
1920

2021
interface IConstructor<T> {
2122
new (...args: DeliberateAny[]): T
@@ -24,8 +25,8 @@ interface IConstructor<T> {
2425
type StateTotals = Pick<Application, 'globalNumBytes' | 'globalNumUint' | 'localNumBytes' | 'localNumUint'>
2526

2627
interface States {
27-
globalStates: Map<bytes, internal.state.GlobalStateCls<unknown>>
28-
localStates: Map<bytes, internal.state.LocalStateMapCls<unknown>>
28+
globalStates: BytesMap<internal.state.GlobalStateCls<unknown>>
29+
localStates: BytesMap<internal.state.LocalStateMapCls<unknown>>
2930
totals: StateTotals
3031
}
3132

@@ -38,8 +39,8 @@ const isUint64GenericType = (typeName: string | undefined) => {
3839
const extractStates = (contract: BaseContract): States => {
3940
const stateTotals = { globalNumBytes: 0, globalNumUint: 0, localNumBytes: 0, localNumUint: 0 }
4041
const states = {
41-
globalStates: new Map<bytes, internal.state.GlobalStateCls<unknown>>(),
42-
localStates: new Map<bytes, internal.state.LocalStateMapCls<unknown>>(),
42+
globalStates: new BytesMap<internal.state.GlobalStateCls<unknown>>(),
43+
localStates: new BytesMap<internal.state.LocalStateMapCls<unknown>>(),
4344
totals: stateTotals,
4445
}
4546
Object.entries(contract).forEach(([key, value]) => {
@@ -50,7 +51,7 @@ const extractStates = (contract: BaseContract): States => {
5051
if (value.key === undefined) value.key = Bytes(key)
5152

5253
// capture state into the context
53-
if (isLocalState) states.localStates.set(value.key, value.map)
54+
if (isLocalState) states.localStates.set(value.key, value)
5455
else states.globalStates.set(value.key, value)
5556

5657
// populate state totals
@@ -64,23 +65,31 @@ const extractStates = (contract: BaseContract): States => {
6465
return states
6566
}
6667

67-
const extractArraysFromArgs = (args: DeliberateAny[]) => {
68+
const extractArraysFromArgs = (app: Application, args: DeliberateAny[]) => {
6869
const transactions: Transaction[] = []
6970
const accounts: Account[] = []
70-
const apps: Application[] = []
71+
const apps: Application[] = [app]
7172
const assets: Asset[] = []
73+
const appArgs: bytes[] = []
74+
75+
// TODO: replace `asUint64Cls(accounts.length).toBytes().asAlgoTs()` with `arc4.Uint8(account.length).toBytes().asAlgoTs()`
7276
for (const arg of args) {
7377
if (isTransaction(arg)) {
7478
transactions.push(arg)
7579
} else if (arg instanceof AccountCls) {
80+
appArgs.push(asUint64Cls(accounts.length).toBytes().asAlgoTs())
7681
accounts.push(arg as Account)
7782
} else if (arg instanceof ApplicationCls) {
83+
appArgs.push(asUint64Cls(apps.length).toBytes().asAlgoTs())
7884
apps.push(arg as Application)
7985
} else if (arg instanceof AssetCls) {
86+
appArgs.push(asUint64Cls(assets.length).toBytes().asAlgoTs())
8087
assets.push(arg as Asset)
8188
}
8289
}
83-
return { accounts, apps, assets, transactions }
90+
91+
// TODO: use actual method selector in appArgs
92+
return { accounts, apps, assets, transactions, appArgs: [Bytes('method_selector'), ...appArgs] }
8493
}
8594

8695
function isTransaction(obj: unknown): obj is Transaction {
@@ -96,7 +105,6 @@ function isTransaction(obj: unknown): obj is Transaction {
96105

97106
export class ContractContext {
98107
create<T extends BaseContract>(type: IConstructor<T>, ...args: DeliberateAny[]): T {
99-
Object.getPrototypeOf(type)
100108
const proxy = new Proxy(type, this.getContractProxyHandler<T>(this.isArc4(type)))
101109
return new proxy(...args)
102110
}
@@ -114,10 +122,12 @@ export class ContractContext {
114122
}
115123

116124
private getContractProxyHandler<T extends BaseContract>(isArc4: boolean): ProxyHandler<IConstructor<T>> {
117-
const onConstructed = (instance: BaseContract) => {
125+
const onConstructed = (instance: T) => {
118126
const states = extractStates(instance)
119127

120128
const application = lazyContext.any.application({
129+
globalStates: states.globalStates,
130+
localStates: states.localStates,
121131
...states.totals,
122132
})
123133
lazyContext.ledger.addAppIdContractMap(application.id, instance)
@@ -127,12 +137,13 @@ export class ContractContext {
127137
const instance = new Proxy(new target(...args), {
128138
get(target, prop, receiver) {
129139
const orig = Reflect.get(target, prop, receiver)
140+
const abiMetadata = getAbiMetadata(target, prop as string)
130141
const isProgramMethod = prop === 'approvalProgram' || prop === 'clearStateProgram'
131-
if (isArc4 || isProgramMethod) {
142+
const isAbiMethod = isArc4 && abiMetadata
143+
if (isAbiMethod || isProgramMethod) {
132144
return (...args: DeliberateAny[]): DeliberateAny => {
133145
const app = lazyContext.ledger.getApplicationForContract(receiver)
134-
const { transactions, ...appCallArgs } = extractArraysFromArgs(args)
135-
const abiMetadata = getAbiMetadata(target, prop as string)
146+
const { transactions, ...appCallArgs } = extractArraysFromArgs(app, args)
136147
const appTxn = lazyContext.any.txn.applicationCall({
137148
appId: app,
138149
...appCallArgs,
@@ -142,7 +153,7 @@ export class ContractContext {
142153
const txns = [...(transactions ?? []), appTxn]
143154
return lazyContext.txn.ensureScope(txns).execute(() => {
144155
const returnValue = (orig as DeliberateAny).apply(target, args)
145-
if (!isProgramMethod && isArc4 && returnValue !== undefined) {
156+
if (!isProgramMethod && isAbiMethod && returnValue !== undefined) {
146157
appTxn.logArc4ReturnValue(returnValue)
147158
}
148159
return returnValue
@@ -152,6 +163,7 @@ export class ContractContext {
152163
return orig
153164
},
154165
})
166+
155167
onConstructed(instance)
156168

157169
return instance

src/subcontexts/ledger-context.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AccountData, AssetHolding } from '../impl/account'
55
import { ApplicationData } from '../impl/application'
66
import { AssetData } from '../impl/asset'
77
import { GlobalData } from '../impl/global'
8-
import { asBigInt, asMaybeUint64Cls, asUint64, asUint64Cls, iterBigInt } from '../util'
8+
import { asBigInt, asBytes, asMaybeBytesCls, asMaybeUint64Cls, asUint64, asUint64Cls, iterBigInt } from '../util'
99

1010
interface BlockData {
1111
seed: bigint
@@ -102,4 +102,36 @@ export class LedgerContext {
102102
}
103103
throw internal.errors.internalError(`Block ${i} not set`)
104104
}
105+
106+
getGlobalState(
107+
app: Application,
108+
key: internal.primitives.StubBytesCompat,
109+
): [internal.state.GlobalStateCls<unknown> | undefined, boolean] {
110+
const appId = asBigInt(app.id)
111+
const keyBytes = asBytes(key)
112+
if (!this.applicationDataMap.has(appId)) {
113+
return [undefined, false]
114+
}
115+
const appData = this.applicationDataMap.get(appId)!
116+
if (!appData.application.globalStates.has(keyBytes)) {
117+
return [undefined, false]
118+
}
119+
return [appData.application.globalStates.get(keyBytes), true]
120+
}
121+
122+
setGlobalState(
123+
app: Application,
124+
key: internal.primitives.StubBytesCompat,
125+
value: internal.primitives.StubUint64Compat | internal.primitives.StubBytesCompat | undefined,
126+
): void {
127+
const appId = asBigInt(app.id)
128+
const keyBytes = asBytes(key)
129+
const appData = this.applicationDataMap.get(appId)!
130+
const globalState = appData.application.globalStates.get(keyBytes)!
131+
if (value === undefined) {
132+
globalState.delete()
133+
} else {
134+
globalState.value = asMaybeUint64Cls(value) ?? asMaybeBytesCls(value)
135+
}
136+
}
105137
}

src/test-execution-context.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { TransactionContext } from './subcontexts/transaction-context'
2121
import { ValueGenerator } from './value-generators'
2222

2323
export class TestExecutionContext implements internal.ExecutionContext {
24-
#applicationLogs: Map<bigint, bytes[]>
2524
#contractContext: ContractContext
2625
#ledgerContext: LedgerContext
2726
#txnContext: TransactionContext
@@ -30,7 +29,6 @@ export class TestExecutionContext implements internal.ExecutionContext {
3029

3130
constructor() {
3231
internal.ctxMgr.instance = this
33-
this.#applicationLogs = new Map()
3432
this.#contractContext = new ContractContext()
3533
this.#ledgerContext = new LedgerContext()
3634
this.#txnContext = new TransactionContext()
@@ -112,7 +110,6 @@ export class TestExecutionContext implements internal.ExecutionContext {
112110
}
113111

114112
reset() {
115-
this.#applicationLogs.clear()
116113
this.#contractContext = new ContractContext()
117114
this.#ledgerContext = new LedgerContext()
118115
this.#txnContext = new TransactionContext()

0 commit comments

Comments
 (0)