diff --git a/examples/rollup.config.ts b/examples/rollup.config.ts index bc04ab8..79ef1aa 100644 --- a/examples/rollup.config.ts +++ b/examples/rollup.config.ts @@ -12,6 +12,7 @@ const config: RollupOptions = { 'examples/auction/contract.algo.ts', 'examples/voting/contract.algo.ts', 'examples/simple-voting/contract.algo.ts', + 'examples/zk-whitelist/contract.algo.ts', ], output: [ { diff --git a/examples/zk-whitelist/contract.algo.ts b/examples/zk-whitelist/contract.algo.ts new file mode 100644 index 0000000..1d575c8 --- /dev/null +++ b/examples/zk-whitelist/contract.algo.ts @@ -0,0 +1,94 @@ +import { + abimethod, + Account, + arc4, + assert, + BigUint, + Bytes, + ensureBudget, + Global, + itxn, + LocalState, + op, + OpUpFeeSource, + TemplateVar, + Txn, + uint64, +} from '@algorandfoundation/algorand-typescript' + +const curveMod = 21888242871839275222246405745257275088548364400416034343698204186575808495617n +const verifierBudget = 145000 + +export default class ZkWhitelistContract extends arc4.Contract { + appName: arc4.Str | undefined + whiteList = LocalState() + + @abimethod({ onCreate: 'require' }) + create(name: arc4.Str) { + // Create the application + this.appName = name + } + + @abimethod({ allowActions: ['UpdateApplication', 'DeleteApplication'] }) + update() { + // Update the application if it is mutable (manager only) + assert(Global.creatorAddress === Txn.sender) + } + + @abimethod({ allowActions: ['OptIn', 'CloseOut'] }) + optInOrOut() { + // Opt in or out of the application + return + } + + @abimethod() + addAddressToWhitelist(address: arc4.Address, proof: arc4.DynamicArray): arc4.Str { + /* + Add caller to the whitelist if the zk proof is valid. + On success, will return an empty string. Otherwise, will return an error + message. + */ + ensureBudget(verifierBudget, OpUpFeeSource.GroupCredit) + // The verifier expects public inputs to be in the curve field, but an + // Algorand address might represent a number larger than the field + // modulus, so to be safe we take the address modulo the field modulus + const addressMod = arc4.interpretAsArc4(op.bzero(32).bitwiseOr(Bytes(BigUint(address.bytes) % curveMod))) + // Verify the proof by calling the deposit verifier app + const verified = this.verifyProof(TemplateVar('VERIFIER_APP_ID'), proof, new arc4.DynamicArray(addressMod)) + if (!verified) { + return new arc4.Str('Proof verification failed') + } + // if successful, add the sender to the whitelist by setting local state + const account = Account(address.bytes) + if (Txn.sender !== account) { + return new arc4.Str('Sender address does not match authorized address') + } + this.whiteList(account).value = true + return new arc4.Str('') + } + + @abimethod() + isOnWhitelist(address: arc4.Address): arc4.Bool { + // Check if an address is on the whitelist + const account = address.native + const optedIn = op.appOptedIn(account, Global.currentApplicationId) + if (!optedIn) { + return new arc4.Bool(false) + } + const whitelisted = this.whiteList(account).value + return new arc4.Bool(whitelisted) + } + + verifyProof(appId: uint64, proof: arc4.DynamicArray, publicInputs: arc4.DynamicArray): arc4.Bool { + // Verify a proof using the verifier app. + const verified = itxn + .applicationCall({ + appId: appId, + fee: 0, + appArgs: [arc4.methodSelector('verify(byte[32][],byte[32][])bool'), proof.copy(), publicInputs.copy()], + onCompletion: arc4.OnCompleteAction.NoOp, + }) + .submit().lastLog + return arc4.interpretAsArc4(verified, 'log') + } +} diff --git a/examples/zk-whitelist/contract.spec.ts b/examples/zk-whitelist/contract.spec.ts new file mode 100644 index 0000000..f9f8bc3 --- /dev/null +++ b/examples/zk-whitelist/contract.spec.ts @@ -0,0 +1,49 @@ +import { arc4, Bytes } from '@algorandfoundation/algorand-typescript' +import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing' +import { afterEach, describe, expect, it } from 'vitest' +import { ABI_RETURN_VALUE_LOG_PREFIX } from '../../src/constants' +import ZkWhitelistContract from './contract.algo' + +describe('ZK Whitelist', () => { + const ctx = new TestExecutionContext() + + afterEach(() => { + ctx.reset() + }) + + it('should be able to add address to whitelist', () => { + // Arrange + const contract = ctx.contract.create(ZkWhitelistContract) + contract.create(ctx.any.arc4.str(10)) + + const address = new arc4.Address(ctx.defaultSender) + const proof = new arc4.DynamicArray(new arc4.Address(Bytes(new Uint8Array(Array(32).fill(0))))) + + const dummyVerifierApp = ctx.any.application({ appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(Bytes.fromHex('80'))] }) + ctx.setTemplateVar('VERIFIER_APP_ID', dummyVerifierApp.id) + + // Act + const result = contract.addAddressToWhitelist(address, proof) + + // Assert + expect(result.native).toEqual('') + expect(contract.whiteList(ctx.defaultSender).value).toEqual(true) + }) + + it('returns error message if proof verification fails', () => { + // Arrange + const contract = ctx.contract.create(ZkWhitelistContract) + contract.create(ctx.any.arc4.str(10)) + + const address = ctx.any.arc4.address() + const proof = new arc4.DynamicArray(new arc4.Address(Bytes(new Uint8Array(Array(32).fill(0))))) + const dummyVerifierApp = ctx.any.application({ appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(Bytes(''))] }) + ctx.setTemplateVar('VERIFIER_APP_ID', dummyVerifierApp.id) + + // Act + const result = contract.addAddressToWhitelist(address, proof) + + // Assert + expect(result.native).toEqual('Proof verification failed') + }) +}) diff --git a/src/constants.ts b/src/constants.ts index 2637b14..b218a66 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,7 +3,11 @@ import { Bytes } from '@algorandfoundation/algorand-typescript' export const UINT64_SIZE = 64 export const UINT512_SIZE = 512 export const MAX_UINT8 = 2 ** 8 - 1 +export const MAX_UINT16 = 2 ** 16 - 1 +export const MAX_UINT32 = 2 ** 32 - 1 export const MAX_UINT64 = 2n ** 64n - 1n +export const MAX_UINT128 = 2n ** 128n - 1n +export const MAX_UINT256 = 2n ** 256n - 1n export const MAX_UINT512 = 2n ** 512n - 1n export const MAX_BYTES_SIZE = 4096 export const MAX_LOG_SIZE = 1024 @@ -43,6 +47,7 @@ export const ABI_RETURN_VALUE_LOG_PREFIX = Bytes.fromHex('151F7C75') export const UINT64_OVERFLOW_UNDERFLOW_MESSAGE = 'Uint64 overflow or underflow' export const BIGUINT_OVERFLOW_UNDERFLOW_MESSAGE = 'BigUint overflow or underflow' +export const DEFAULT_TEMPLATE_VAR_PREFIX = 'TMPL_' export const APP_ID_PREFIX = 'appID' export const HASH_BYTES_LENGTH = 32 diff --git a/src/impl/acct-params.ts b/src/impl/acct-params.ts index 2864405..9042446 100644 --- a/src/impl/acct-params.ts +++ b/src/impl/acct-params.ts @@ -1,6 +1,7 @@ -import { Account, gtxn, internal, uint64 } from '@algorandfoundation/algorand-typescript' +import { Account, Application, gtxn, internal, uint64 } from '@algorandfoundation/algorand-typescript' import { lazyContext } from '../context-helpers/internal-context' import { asMaybeUint64Cls } from '../util' +import { getApp } from './app-params' export const getAccount = (acct: Account | internal.primitives.StubUint64Compat): Account => { const acctId = asMaybeUint64Cls(acct) @@ -21,6 +22,19 @@ export const minBalance = (a: Account | internal.primitives.StubUint64Compat): u return acct.minBalance } +export const appOptedIn = ( + a: Account | internal.primitives.StubUint64Compat, + b: Application | internal.primitives.StubUint64Compat, +): boolean => { + const account = getAccount(a) + const app = getApp(b) + + if (account === undefined || app === undefined) { + return false + } + return account.isOptedIn(app) +} + export const AcctParams: internal.opTypes.AcctParamsType = { acctBalance(a: Account | internal.primitives.StubUint64Compat): readonly [uint64, boolean] { const acct = getAccount(a) diff --git a/src/impl/ensure-budget.ts b/src/impl/ensure-budget.ts new file mode 100644 index 0000000..3ebb307 --- /dev/null +++ b/src/impl/ensure-budget.ts @@ -0,0 +1,5 @@ +import { OpUpFeeSource, uint64 } from '@algorandfoundation/algorand-typescript' + +export function ensureBudgetImpl(_budget: uint64, _feeSource: OpUpFeeSource = OpUpFeeSource.GroupCredit) { + // ensureBudget function is emulated to be a no-op +} diff --git a/src/impl/index.ts b/src/impl/index.ts index 2909910..42bd62b 100644 --- a/src/impl/index.ts +++ b/src/impl/index.ts @@ -1,4 +1,4 @@ -export { AcctParams, balance, minBalance } from './acct-params' +export { AcctParams, appOptedIn, balance, minBalance } from './acct-params' export { AppGlobal } from './app-global' export { AppLocal } from './app-local' export { AppParams } from './app-params' diff --git a/src/impl/template-var.ts b/src/impl/template-var.ts new file mode 100644 index 0000000..e49cfc4 --- /dev/null +++ b/src/impl/template-var.ts @@ -0,0 +1,11 @@ +import { internal } from '@algorandfoundation/algorand-typescript' +import { DEFAULT_TEMPLATE_VAR_PREFIX } from '../constants' +import { lazyContext } from '../context-helpers/internal-context' + +export function TemplateVarImpl(variableName: string, prefix = DEFAULT_TEMPLATE_VAR_PREFIX): T { + const key = prefix + variableName + if (!Object.hasOwn(lazyContext.value.templateVars, key)) { + throw internal.errors.codeError(`Template variable ${key} not found in test context!`) + } + return lazyContext.value.templateVars[prefix + variableName] as T +} diff --git a/src/runtime-helpers.ts b/src/runtime-helpers.ts index 10e58cc..48b08d6 100644 --- a/src/runtime-helpers.ts +++ b/src/runtime-helpers.ts @@ -8,6 +8,8 @@ import { nameOfType } from './util' export { attachAbiMetadata } from './abi-metadata' export * from './impl/encoded-types' +export { ensureBudgetImpl } from './impl/ensure-budget' +export { TemplateVarImpl } from './impl/template-var' export function switchableValue(x: unknown): bigint | string | boolean { if (typeof x === 'boolean') return x @@ -92,6 +94,7 @@ function arc4EncodedOp(left: ARC4Encoded, right: ARC4Encoded, op: BinaryOps): De function accountBinaryOp(left: AccountCls, right: AccountCls, op: BinaryOps): DeliberateAny { switch (op) { case '===': + case '!==': return bytesBinaryOp(left.bytes, right.bytes, op) default: internal.errors.internalError(`Unsupported operator ${op}`) diff --git a/src/test-execution-context.ts b/src/test-execution-context.ts index c7723c4..efbf68e 100644 --- a/src/test-execution-context.ts +++ b/src/test-execution-context.ts @@ -1,5 +1,6 @@ -import { Account, Application, Asset, Bytes, bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript' +import { Account, Application, Asset, bytes, internal, LogicSig, uint64 } from '@algorandfoundation/algorand-typescript' import { captureMethodConfig } from './abi-metadata' +import { DEFAULT_TEMPLATE_VAR_PREFIX } from './constants' import { DecodedLogs, LogDecoding } from './decode-logs' import * as ops from './impl' import { AccountCls } from './impl/account' @@ -18,6 +19,7 @@ import { Box, BoxMap, BoxRef, GlobalState, LocalState } from './impl/state' import { ContractContext } from './subcontexts/contract-context' import { LedgerContext } from './subcontexts/ledger-context' import { TransactionContext } from './subcontexts/transaction-context' +import { DeliberateAny } from './typescript-helpers' import { getRandomBytes } from './util' import { ValueGenerator } from './value-generators' @@ -28,6 +30,7 @@ export class TestExecutionContext implements internal.ExecutionContext { #valueGenerator: ValueGenerator #defaultSender: Account #activeLogicSigArgs: bytes[] + #template_vars: Record = {} constructor(defaultSenderAddress?: bytes) { internal.ctxMgr.instance = this @@ -126,6 +129,10 @@ export class TestExecutionContext implements internal.ExecutionContext { return this.#activeLogicSigArgs } + get templateVars(): Record { + return this.#template_vars + } + executeLogicSig(logicSig: LogicSig, ...args: bytes[]): boolean | uint64 { this.#activeLogicSigArgs = args try { @@ -135,10 +142,16 @@ export class TestExecutionContext implements internal.ExecutionContext { } } + setTemplateVar(name: string, value: DeliberateAny) { + this.#template_vars[DEFAULT_TEMPLATE_VAR_PREFIX + name] = value + } + reset() { this.#contractContext = new ContractContext() this.#ledgerContext = new LedgerContext() this.#txnContext = new TransactionContext() + this.#activeLogicSigArgs = [] + this.#template_vars = {} internal.ctxMgr.reset() internal.ctxMgr.instance = this } diff --git a/src/test-transformer/node-factory.ts b/src/test-transformer/node-factory.ts index 49d5607..684c734 100644 --- a/src/test-transformer/node-factory.ts +++ b/src/test-transformer/node-factory.ts @@ -94,12 +94,11 @@ export const nodeFactory = { ) }, - callARC4EncodingUtil(node: ts.CallExpression, typeInfo?: TypeInfo) { - const identifierExpression = node.expression as ts.Identifier + callStubbedFunction(functionName: string, node: ts.CallExpression, typeInfo?: TypeInfo) { const infoString = JSON.stringify(typeInfo) const updatedPropertyAccessExpression = factory.createPropertyAccessExpression( factory.createIdentifier('runtimeHelpers'), - `${identifierExpression.getText()}Impl`, + `${functionName}Impl`, ) return factory.createCallExpression( diff --git a/src/test-transformer/visitors.ts b/src/test-transformer/visitors.ts index 76e5edf..94b0681 100644 --- a/src/test-transformer/visitors.ts +++ b/src/test-transformer/visitors.ts @@ -12,10 +12,13 @@ import { const { factory } = ts +const algotsModulePaths = ['@algorandfoundation/algorand-typescript', '/puya-ts/packages/algo-ts/'] + type VisitorHelper = { additionalStatements: ts.Statement[] resolveType(node: ts.Node): ptypes.PType sourceLocation(node: ts.Node): SourceLocation + tryGetSymbol(node: ts.Node): ts.Symbol | undefined } export class SourceFileVisitor { @@ -26,8 +29,8 @@ export class SourceFileVisitor { program: ts.Program, private config: TransformerConfig, ) { - const typeResolver = new TypeResolver(program.getTypeChecker(), program.getCurrentDirectory()) - + const typeChecker = program.getTypeChecker() + const typeResolver = new TypeResolver(typeChecker, program.getCurrentDirectory()) this.helper = { additionalStatements: [], resolveType(node: ts.Node): ptypes.PType { @@ -37,6 +40,10 @@ export class SourceFileVisitor { return ptypes.anyPType } }, + tryGetSymbol(node: ts.Node): ts.Symbol | undefined { + const s = typeChecker.getSymbolAtLocation(node) + return s && s.flags & ts.SymbolFlags.Alias ? typeChecker.getAliasedSymbol(s) : s + }, sourceLocation(node: ts.Node): SourceLocation { return SourceLocation.fromNode(node, program.getCurrentDirectory()) }, @@ -45,7 +52,6 @@ export class SourceFileVisitor { public result(): ts.SourceFile { const updatedSourceFile = ts.visitNode(this.sourceFile, this.visit) as ts.SourceFile - return factory.updateSourceFile(updatedSourceFile, [ nodeFactory.importHelpers(this.config.testingPackageName), ...updatedSourceFile.statements, @@ -91,20 +97,21 @@ class ExpressionVisitor { const isGeneric = isGenericType(type) const isArc4Encoded = isArc4EncodedType(type) - if (isGeneric || isArc4Encoded) { - let updatedNode = node - const info = getGenericTypeInfo(type) + const info = isGeneric || isArc4Encoded ? getGenericTypeInfo(type) : undefined + let updatedNode = node + + if (ts.isNewExpression(updatedNode)) { if (isArc4EncodedType(type)) { - if (ts.isNewExpression(updatedNode)) { - updatedNode = nodeFactory.instantiateARC4EncodedType(updatedNode, info) - } else if (ts.isCallExpression(updatedNode) && isCallingARC4EncodingUtils(updatedNode)) { - updatedNode = nodeFactory.callARC4EncodingUtil(updatedNode, info) - } + updatedNode = nodeFactory.instantiateARC4EncodedType(updatedNode, info) } - return isGeneric - ? nodeFactory.captureGenericTypeInfo(ts.visitEachChild(updatedNode, this.visit, this.context), JSON.stringify(info)) - : ts.visitEachChild(updatedNode, this.visit, this.context) } + if (ts.isCallExpression(updatedNode)) { + const stubbedFunctionName = tryGetStubbedFunctionName(updatedNode, this.helper) + updatedNode = stubbedFunctionName ? nodeFactory.callStubbedFunction(stubbedFunctionName, updatedNode, info) : updatedNode + } + return isGeneric + ? nodeFactory.captureGenericTypeInfo(ts.visitEachChild(updatedNode, this.visit, this.context), JSON.stringify(info)) + : ts.visitEachChild(updatedNode, this.visit, this.context) } return ts.visitEachChild(node, this.visit, this.context) } @@ -194,7 +201,7 @@ class FunctionOrMethodVisitor { if (ts.isNewExpression(node)) { return new ExpressionVisitor(this.context, this.helper, node).result() } - if (ts.isCallExpression(node) && isCallingARC4EncodingUtils(node)) { + if (ts.isCallExpression(node) && tryGetStubbedFunctionName(node, this.helper)) { return new ExpressionVisitor(this.context, this.helper, node).result() } @@ -331,9 +338,17 @@ const getGenericTypeInfo = (type: ptypes.PType): TypeInfo => { return result } -const isCallingARC4EncodingUtils = (node: ts.CallExpression) => { - if (node.expression.kind !== ts.SyntaxKind.Identifier) return false - const identityExpression = node.expression as ts.Identifier - const utilMethods = ['interpretAsArc4', 'decodeArc4', 'encodeArc4'] - return utilMethods.includes(identityExpression.text) +const tryGetStubbedFunctionName = (node: ts.CallExpression, helper: VisitorHelper): string | undefined => { + if (node.expression.kind !== ts.SyntaxKind.Identifier && !ts.isPropertyAccessExpression(node.expression)) return undefined + const identityExpression = ts.isPropertyAccessExpression(node.expression) + ? (node.expression as ts.PropertyAccessExpression).name + : (node.expression as ts.Identifier) + const functionSymbol = helper.tryGetSymbol(identityExpression) + if (functionSymbol) { + const sourceFileName = functionSymbol.valueDeclaration?.getSourceFile().fileName + if (sourceFileName && !algotsModulePaths.some((s) => sourceFileName.includes(s))) return undefined + } + const functionName = functionSymbol?.getName() ?? identityExpression.text + const stubbedFunctionNames = ['interpretAsArc4', 'decodeArc4', 'encodeArc4', 'TemplateVar', 'ensureBudget'] + return stubbedFunctionNames.includes(functionName) ? functionName : undefined } diff --git a/src/value-generators/arc4.ts b/src/value-generators/arc4.ts new file mode 100644 index 0000000..3afbb07 --- /dev/null +++ b/src/value-generators/arc4.ts @@ -0,0 +1,111 @@ +import { arc4 } from '@algorandfoundation/algorand-typescript' +import { BITS_IN_BYTE, MAX_UINT128, MAX_UINT16, MAX_UINT256, MAX_UINT32, MAX_UINT512, MAX_UINT64, MAX_UINT8 } from '../constants' +import { AddressImpl } from '../runtime-helpers' +import { getRandomBigInt, getRandomBytes, getRandomNumber } from '../util' +import { AvmValueGenerator } from './avm' + +export class Arc4ValueGenerator { + /** + * Generate a random Algorand address. + * @returns: A new, random Algorand address. + * */ + address(): arc4.Address { + const source = new AvmValueGenerator().account() + const result = new AddressImpl({ name: 'StaticArray', genericArgs: { elementType: { name: 'Bytes' }, size: { name: '32' } } }, source) + return result + } + + /** + * Generate a random UintN8 within the specified range. + * @param minValue: Minimum value (inclusive). Defaults to 0. + * @param maxValue: Maximum value (inclusive). Defaults to MAX_UINT8. + * @returns: A random UintN8 value. + * */ + uintN8(minValue = 0, maxValue = MAX_UINT8): arc4.UintN8 { + return new arc4.UintN8(getRandomNumber(minValue, maxValue)) + } + + /** + * Generate a random UintN16 within the specified range. + * @param minValue: Minimum value (inclusive). Defaults to 0. + * @param maxValue: Maximum value (inclusive). Defaults to MAX_UINT16. + * @returns: A random UintN16 value. + * */ + uintN16(minValue = 0, maxValue = MAX_UINT16): arc4.UintN16 { + return new arc4.UintN16(getRandomNumber(minValue, maxValue)) + } + + /** + * Generate a random UintN32 within the specified range. + * @param minValue: Minimum value (inclusive). Defaults to 0. + * @param maxValue: Maximum value (inclusive). Defaults to MAX_UINT32. + * @returns: A random UintN32 value. + * */ + uintN32(minValue = 0, maxValue = MAX_UINT32): arc4.UintN32 { + return new arc4.UintN32(getRandomNumber(minValue, maxValue)) + } + + /** + * Generate a random UintN64 within the specified range. + * @param minValue: Minimum value (inclusive). Defaults to 0. + * @param maxValue: Maximum value (inclusive). Defaults to MAX_UINT64. + * @returns: A random UintN64 value. + * */ + uintN64(minValue = 0, maxValue = MAX_UINT64): arc4.UintN64 { + return new arc4.UintN64(getRandomBigInt(minValue, maxValue)) + } + + /** + * Generate a random UintN128 within the specified range. + * @param minValue: Minimum value (inclusive). Defaults to 0. + * @param maxValue: Maximum value (inclusive). Defaults to MAX_UINT128. + * @returns: A random UintN128 value. + * */ + uintN128(minValue = 0, maxValue = MAX_UINT128): arc4.UintN128 { + return new arc4.UintN128(getRandomBigInt(minValue, maxValue)) + } + + /** + * Generate a random UintN256 within the specified range. + * @param minValue: Minimum value (inclusive). Defaults to 0. + * @param maxValue: Maximum value (inclusive). Defaults to MAX_UINT256. + * @returns: A random UintN256 value. + * */ + uintN256(minValue = 0, maxValue = MAX_UINT256): arc4.UintN256 { + return new arc4.UintN256(getRandomBigInt(minValue, maxValue)) + } + + /** + * Generate a random UintN512 within the specified range. + * @param minValue: Minimum value (inclusive). Defaults to 0. + * @param maxValue: Maximum value (inclusive). Defaults to MAX_UINT512. + * @returns: A random UintN512 value. + * */ + uintN512(minValue = 0, maxValue = MAX_UINT512): arc4.UintN<512> { + return new arc4.UintN<512>(getRandomBigInt(minValue, maxValue)) + } + + /** + * Generate a random dynamic bytes of size `n` bits. + * @param n: The number of bits for the dynamic bytes. Must be a multiple of 8, otherwise + * the last byte will be truncated. + * @returns: A new, random dynamic bytes of size `n` bits. + * */ + dynamicBytes(n: number): arc4.DynamicBytes { + return new arc4.DynamicBytes(getRandomBytes(n / BITS_IN_BYTE).asAlgoTs()) + } + + /** + * Generate a random dynamic string of size `n` bits. + * @param n: The number of bits for the string. + * @returns: A new, random string of size `n` bits. + * */ + str(n: number): arc4.Str { + // Calculate the number of characters needed (rounding up) + const numChars = n + 7 // 8 + + // Generate random string + const bytes = getRandomBytes(numChars) + return new arc4.Str(bytes.toString()) + } +} diff --git a/src/value-generators/index.ts b/src/value-generators/index.ts index ad3fe82..f077945 100644 --- a/src/value-generators/index.ts +++ b/src/value-generators/index.ts @@ -1,11 +1,14 @@ +import { Arc4ValueGenerator } from './arc4' import { AvmValueGenerator } from './avm' import { TxnValueGenerator } from './txn' export class ValueGenerator extends AvmValueGenerator { txn: TxnValueGenerator + arc4: Arc4ValueGenerator constructor() { super() this.txn = new TxnValueGenerator() + this.arc4 = new Arc4ValueGenerator() } }