Skip to content

Commit 6685c92

Browse files
committed
feat: implement stubs for appOptedIn, ensureBudget, TemplateVar
refactor: function call replacement (...Impl) in visitor to be more resilient by catering for the following cases - module name prefix (e.g. `arc4.interpretAsArc4`) - import name alias (e.g. `import { interpretAsArc4 as interpret } from '@algorandfoundation/algorand-typescript/arc4'`) - function name conflict (e.g. ``` import { interpretAsArc4 as interpret } .... const interpretAsArc4 = () => {...} ```) feat: add arc4 value generator feat: add zk-whitelist example contract and tests
1 parent 2084c95 commit 6685c92

File tree

14 files changed

+386
-26
lines changed

14 files changed

+386
-26
lines changed

examples/rollup.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const config: RollupOptions = {
1212
'examples/auction/contract.algo.ts',
1313
'examples/voting/contract.algo.ts',
1414
'examples/simple-voting/contract.algo.ts',
15+
'examples/zk-whitelist/contract.algo.ts',
1516
],
1617
output: [
1718
{
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {
2+
abimethod,
3+
Account,
4+
arc4,
5+
assert,
6+
BigUint,
7+
Bytes,
8+
ensureBudget,
9+
Global,
10+
itxn,
11+
LocalState,
12+
op,
13+
OpUpFeeSource,
14+
TemplateVar,
15+
Txn,
16+
uint64,
17+
} from '@algorandfoundation/algorand-typescript'
18+
19+
const curveMod = 21888242871839275222246405745257275088548364400416034343698204186575808495617n
20+
const verifierBudget = 145000
21+
22+
export default class ZkWhitelistContract extends arc4.Contract {
23+
appName: arc4.Str | undefined
24+
whiteList = LocalState<boolean>()
25+
26+
@abimethod({ onCreate: 'require' })
27+
create(name: arc4.Str) {
28+
// Create the application
29+
this.appName = name
30+
}
31+
32+
@abimethod({ allowActions: ['UpdateApplication', 'DeleteApplication'] })
33+
update() {
34+
// Update the application if it is mutable (manager only)
35+
assert(Global.creatorAddress === Txn.sender)
36+
}
37+
38+
@abimethod({ allowActions: ['OptIn', 'CloseOut'] })
39+
optInOrOut() {
40+
// Opt in or out of the application
41+
return
42+
}
43+
44+
@abimethod()
45+
addAddressToWhitelist(address: arc4.Address, proof: arc4.DynamicArray<arc4.Address>): arc4.Str {
46+
/*
47+
Add caller to the whitelist if the zk proof is valid.
48+
On success, will return an empty string. Otherwise, will return an error
49+
message.
50+
*/
51+
ensureBudget(verifierBudget, OpUpFeeSource.GroupCredit)
52+
// The verifier expects public inputs to be in the curve field, but an
53+
// Algorand address might represent a number larger than the field
54+
// modulus, so to be safe we take the address modulo the field modulus
55+
const addressMod = arc4.interpretAsArc4<arc4.Address>(op.bzero(32).bitwiseOr(Bytes(BigUint(address.bytes) % curveMod)))
56+
// Verify the proof by calling the deposit verifier app
57+
const verified = this.verifyProof(TemplateVar<uint64>('VERIFIER_APP_ID'), proof, new arc4.DynamicArray(addressMod))
58+
if (!verified.native) {
59+
return new arc4.Str('Proof verification failed')
60+
}
61+
// if successful, add the sender to the whitelist by setting local state
62+
const account = Account(address.bytes)
63+
if (Txn.sender !== account) {
64+
return new arc4.Str('Sender address does not match authorized address')
65+
}
66+
this.whiteList(account).value = true
67+
return new arc4.Str('')
68+
}
69+
70+
@abimethod()
71+
isOnWhitelist(address: arc4.Address): arc4.Bool {
72+
// Check if an address is on the whitelist
73+
const account = address.native
74+
const optedIn = op.appOptedIn(account, Global.currentApplicationId)
75+
if (!optedIn) {
76+
return new arc4.Bool(false)
77+
}
78+
const whitelisted = this.whiteList(account).value
79+
return new arc4.Bool(whitelisted)
80+
}
81+
82+
verifyProof(appId: uint64, proof: arc4.DynamicArray<arc4.Address>, publicInputs: arc4.DynamicArray<arc4.Address>): arc4.Bool {
83+
// Verify a proof using the verifier app.
84+
const verified = itxn
85+
.applicationCall({
86+
appId: appId,
87+
fee: 0,
88+
appArgs: [arc4.methodSelector('verify(byte[32][],byte[32][])bool'), proof.copy(), publicInputs.copy()],
89+
onCompletion: arc4.OnCompleteAction.NoOp,
90+
})
91+
.submit().lastLog
92+
return arc4.interpretAsArc4<arc4.Bool>(verified, 'log')
93+
}
94+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { arc4, Bytes } from '@algorandfoundation/algorand-typescript'
2+
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
3+
import { afterEach, describe, expect, it } from 'vitest'
4+
import { ABI_RETURN_VALUE_LOG_PREFIX } from '../../src/constants'
5+
import ZkWhitelistContract from './contract.algo'
6+
7+
describe('ZK Whitelist', () => {
8+
const ctx = new TestExecutionContext()
9+
10+
afterEach(() => {
11+
ctx.reset()
12+
})
13+
14+
it('should be able to add address to whitelist', () => {
15+
// Arrange
16+
const contract = ctx.contract.create(ZkWhitelistContract)
17+
contract.create(ctx.any.arc4.str(10))
18+
19+
const address = new arc4.Address(ctx.defaultSender)
20+
const proof = new arc4.DynamicArray<arc4.Address>(new arc4.Address(Bytes(new Uint8Array(Array(32).fill(0)))))
21+
22+
const dummyVerifierApp = ctx.any.application({ appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(Bytes.fromHex('80'))] })
23+
ctx.setTemplateVar('VERIFIER_APP_ID', dummyVerifierApp.id)
24+
25+
// Act
26+
const result = contract.addAddressToWhitelist(address, proof)
27+
28+
// Assert
29+
expect(result.native).toEqual('')
30+
expect(contract.whiteList(ctx.defaultSender).value).toEqual(true)
31+
})
32+
33+
it('returns error message if proof verification fails', () => {
34+
// Arrange
35+
const contract = ctx.contract.create(ZkWhitelistContract)
36+
contract.create(ctx.any.arc4.str(10))
37+
38+
const address = ctx.any.arc4.address()
39+
const proof = new arc4.DynamicArray<arc4.Address>(new arc4.Address(Bytes(new Uint8Array(Array(32).fill(0)))))
40+
const dummyVerifierApp = ctx.any.application({ appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(Bytes(''))] })
41+
ctx.setTemplateVar('VERIFIER_APP_ID', dummyVerifierApp.id)
42+
43+
// Act
44+
const result = contract.addAddressToWhitelist(address, proof)
45+
46+
// Assert
47+
expect(result.native).toEqual('Proof verification failed')
48+
})
49+
50+
it('returns true if address is already in whitelist', () => {
51+
// Arrange
52+
const contract = ctx.contract.create(ZkWhitelistContract)
53+
contract.create(ctx.any.arc4.str(10))
54+
55+
const dummyAccount = ctx.any.account({ optedApplications: [ctx.ledger.getApplicationForContract(contract)] })
56+
contract.whiteList(dummyAccount).value = true
57+
58+
// Act
59+
const result = contract.isOnWhitelist(new arc4.Address(dummyAccount))
60+
61+
// Assert
62+
expect(result.native).toBe(true)
63+
})
64+
65+
it('returns false if address is not in whitelist', () => {
66+
// Arrange
67+
const contract = ctx.contract.create(ZkWhitelistContract)
68+
contract.create(ctx.any.arc4.str(10))
69+
70+
const dummyAccount = ctx.any.account({ optedApplications: [ctx.ledger.getApplicationForContract(contract)] })
71+
contract.whiteList(dummyAccount).value = false
72+
73+
// Act
74+
const result = contract.isOnWhitelist(new arc4.Address(dummyAccount))
75+
76+
// Assert
77+
expect(result.native).toBe(false)
78+
})
79+
})

src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { Bytes } from '@algorandfoundation/algorand-typescript'
33
export const UINT64_SIZE = 64
44
export const UINT512_SIZE = 512
55
export const MAX_UINT8 = 2 ** 8 - 1
6+
export const MAX_UINT16 = 2 ** 16 - 1
7+
export const MAX_UINT32 = 2 ** 32 - 1
68
export const MAX_UINT64 = 2n ** 64n - 1n
9+
export const MAX_UINT128 = 2n ** 128n - 1n
10+
export const MAX_UINT256 = 2n ** 256n - 1n
711
export const MAX_UINT512 = 2n ** 512n - 1n
812
export const MAX_BYTES_SIZE = 4096
913
export const MAX_LOG_SIZE = 1024
@@ -43,6 +47,7 @@ export const ABI_RETURN_VALUE_LOG_PREFIX = Bytes.fromHex('151F7C75')
4347

4448
export const UINT64_OVERFLOW_UNDERFLOW_MESSAGE = 'Uint64 overflow or underflow'
4549
export const BIGUINT_OVERFLOW_UNDERFLOW_MESSAGE = 'BigUint overflow or underflow'
50+
export const DEFAULT_TEMPLATE_VAR_PREFIX = 'TMPL_'
4651

4752
export const APP_ID_PREFIX = 'appID'
4853
export const HASH_BYTES_LENGTH = 32

src/impl/acct-params.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Account, gtxn, internal, uint64 } from '@algorandfoundation/algorand-typescript'
1+
import { Account, Application, gtxn, internal, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { lazyContext } from '../context-helpers/internal-context'
33
import { asMaybeUint64Cls } from '../util'
4+
import { getApp } from './app-params'
45

56
export const getAccount = (acct: Account | internal.primitives.StubUint64Compat): Account => {
67
const acctId = asMaybeUint64Cls(acct)
@@ -21,6 +22,19 @@ export const minBalance = (a: Account | internal.primitives.StubUint64Compat): u
2122
return acct.minBalance
2223
}
2324

25+
export const appOptedIn = (
26+
a: Account | internal.primitives.StubUint64Compat,
27+
b: Application | internal.primitives.StubUint64Compat,
28+
): boolean => {
29+
const account = getAccount(a)
30+
const app = getApp(b)
31+
32+
if (account === undefined || app === undefined) {
33+
return false
34+
}
35+
return account.isOptedIn(app)
36+
}
37+
2438
export const AcctParams: internal.opTypes.AcctParamsType = {
2539
acctBalance(a: Account | internal.primitives.StubUint64Compat): readonly [uint64, boolean] {
2640
const acct = getAccount(a)

src/impl/ensure-budget.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { OpUpFeeSource, uint64 } from '@algorandfoundation/algorand-typescript'
2+
3+
export function ensureBudgetImpl(_budget: uint64, _feeSource: OpUpFeeSource = OpUpFeeSource.GroupCredit) {
4+
// ensureBudget function is emulated to be a no-op
5+
}

src/impl/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { AcctParams, balance, minBalance } from './acct-params'
1+
export { AcctParams, appOptedIn, balance, minBalance } from './acct-params'
22
export { AppGlobal } from './app-global'
33
export { AppLocal } from './app-local'
44
export { AppParams } from './app-params'

src/impl/template-var.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { internal } from '@algorandfoundation/algorand-typescript'
2+
import { DEFAULT_TEMPLATE_VAR_PREFIX } from '../constants'
3+
import { lazyContext } from '../context-helpers/internal-context'
4+
5+
export function TemplateVarImpl<T>(variableName: string, prefix = DEFAULT_TEMPLATE_VAR_PREFIX): T {
6+
const key = prefix + variableName
7+
if (!Object.hasOwn(lazyContext.value.templateVars, key)) {
8+
throw internal.errors.codeError(`Template variable ${key} not found in test context!`)
9+
}
10+
return lazyContext.value.templateVars[prefix + variableName] as T
11+
}

src/runtime-helpers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { nameOfType } from './util'
88

99
export { attachAbiMetadata } from './abi-metadata'
1010
export * from './impl/encoded-types'
11+
export { ensureBudgetImpl } from './impl/ensure-budget'
12+
export { TemplateVarImpl } from './impl/template-var'
1113

1214
export function switchableValue(x: unknown): bigint | string | boolean {
1315
if (typeof x === 'boolean') return x
@@ -92,6 +94,7 @@ function arc4EncodedOp(left: ARC4Encoded, right: ARC4Encoded, op: BinaryOps): De
9294
function accountBinaryOp(left: AccountCls, right: AccountCls, op: BinaryOps): DeliberateAny {
9395
switch (op) {
9496
case '===':
97+
case '!==':
9598
return bytesBinaryOp(left.bytes, right.bytes, op)
9699
default:
97100
internal.errors.internalError(`Unsupported operator ${op}`)

src/test-execution-context.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { Account, Application, Asset, Bytes, bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript'
1+
import { Account, Application, Asset, bytes, internal, LogicSig, uint64 } from '@algorandfoundation/algorand-typescript'
22
import { captureMethodConfig } from './abi-metadata'
3+
import { DEFAULT_TEMPLATE_VAR_PREFIX } from './constants'
34
import { DecodedLogs, LogDecoding } from './decode-logs'
45
import * as ops from './impl'
56
import { AccountCls } from './impl/account'
@@ -18,6 +19,7 @@ import { Box, BoxMap, BoxRef, GlobalState, LocalState } from './impl/state'
1819
import { ContractContext } from './subcontexts/contract-context'
1920
import { LedgerContext } from './subcontexts/ledger-context'
2021
import { TransactionContext } from './subcontexts/transaction-context'
22+
import { DeliberateAny } from './typescript-helpers'
2123
import { getRandomBytes } from './util'
2224
import { ValueGenerator } from './value-generators'
2325

@@ -28,6 +30,7 @@ export class TestExecutionContext implements internal.ExecutionContext {
2830
#valueGenerator: ValueGenerator
2931
#defaultSender: Account
3032
#activeLogicSigArgs: bytes[]
33+
#template_vars: Record<string, DeliberateAny> = {}
3134

3235
constructor(defaultSenderAddress?: bytes) {
3336
internal.ctxMgr.instance = this
@@ -126,6 +129,10 @@ export class TestExecutionContext implements internal.ExecutionContext {
126129
return this.#activeLogicSigArgs
127130
}
128131

132+
get templateVars(): Record<string, DeliberateAny> {
133+
return this.#template_vars
134+
}
135+
129136
executeLogicSig(logicSig: LogicSig, ...args: bytes[]): boolean | uint64 {
130137
this.#activeLogicSigArgs = args
131138
try {
@@ -135,10 +142,16 @@ export class TestExecutionContext implements internal.ExecutionContext {
135142
}
136143
}
137144

145+
setTemplateVar(name: string, value: DeliberateAny) {
146+
this.#template_vars[DEFAULT_TEMPLATE_VAR_PREFIX + name] = value
147+
}
148+
138149
reset() {
139150
this.#contractContext = new ContractContext()
140151
this.#ledgerContext = new LedgerContext()
141152
this.#txnContext = new TransactionContext()
153+
this.#activeLogicSigArgs = []
154+
this.#template_vars = {}
142155
internal.ctxMgr.reset()
143156
internal.ctxMgr.instance = this
144157
}

0 commit comments

Comments
 (0)