diff --git a/examples/voting/contract.algo.ts b/examples/voting/contract.algo.ts index 53a43e30..eb0b9bbb 100644 --- a/examples/voting/contract.algo.ts +++ b/examples/voting/contract.algo.ts @@ -33,7 +33,6 @@ const BOX_BYTE_MIN_BALANCE: uint64 = 400 // The min balance increase for each asset opted into const ASSET_MIN_BALANCE: uint64 = 100000 -// TODO: ObjectPType should hopefully respect this ordering of properties type VotingPreconditions = { is_voting_open: uint64 is_allowed_to_vote: uint64 diff --git a/package-lock.json b/package-lock.json index c87c8607..9f996354 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@algorandfoundation/algokit-utils": "^7.0.0-beta.7", "@algorandfoundation/algorand-typescript": "file:packages/algo-ts/dist", "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", @@ -73,6 +74,22 @@ "tslib": "^2.6.2" } }, + "node_modules/@algorandfoundation/algokit-utils": { + "version": "7.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-utils/-/algokit-utils-7.0.0-beta.7.tgz", + "integrity": "sha512-xueS9bYnboF3x4Zp/7mCgkLZQR91b5NOuM6aZawv5ySZ+GrRqqZnThJYcGVSNG5GSlGp1ypkEi3oXeApmwdGCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "algosdk": "^2.7.0" + } + }, "node_modules/@algorandfoundation/algorand-typescript": { "resolved": "packages/algo-ts/dist", "link": true @@ -2897,6 +2914,39 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/algo-msgpack-with-bigint": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/algo-msgpack-with-bigint/-/algo-msgpack-with-bigint-2.1.1.tgz", + "integrity": "sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/algosdk": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/algosdk/-/algosdk-2.9.0.tgz", + "integrity": "sha512-o0n0nLMbTX6SFQdMUk2/2sy50jmEmZk5OTPYSh2aAeP8DUPxrhjMPfwGsYNvaO+qk75MixC2eWpfA9vygCQ/Mg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "algo-msgpack-with-bigint": "^2.1.1", + "buffer": "^6.0.3", + "hi-base32": "^0.5.1", + "js-sha256": "^0.9.0", + "js-sha3": "^0.8.0", + "js-sha512": "^0.8.0", + "json-bigint": "^1.0.0", + "tweetnacl": "^1.0.3", + "vlq": "^2.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/ansi-escapes": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", @@ -3047,6 +3097,27 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/before-after-hook": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", @@ -3084,6 +3155,17 @@ "node": ">= 12" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -3115,6 +3197,31 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -5354,6 +5461,14 @@ "node": ">= 0.4" } }, + "node_modules/hi-base32": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", + "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -5429,6 +5544,27 @@ "node": ">=18.18.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6066,6 +6202,30 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6086,6 +6246,17 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -11807,6 +11978,14 @@ "fsevents": "~2.3.3" } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true, + "license": "Unlicense", + "peer": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12652,6 +12831,14 @@ } } }, + "node_modules/vlq": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-2.0.4.tgz", + "integrity": "sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index ba51e679..0aed6c94 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dev:calculator": "tsx src/cli.ts build examples/calculator/contract.algo.ts", "dev:approvals": "tsx src/cli.ts build tests/approvals --output-awst --output-awst-json --no-output-teal --output-ssa-ir --out-dir out/[name]", "dev:expected-output": "tsx src/cli.ts build tests/expected-output --dry-run", - "dev:testing": "tsx src/cli.ts build tests/approvals/destructuring-iterators.algo.ts --output-awst --output-awst-json --log-level=info --log-level debug", + "dev:testing": "tsx src/cli.ts build tests/expected-output/cant-create.algo.ts --output-awst --output-awst-json --log-level=info --log-level debug", "audit": "better-npm-audit audit", "format": "prettier --write .", "lint": "eslint \"src/**/*.ts\"", @@ -45,6 +45,7 @@ "author": "Algorand foundation", "license": "MIT", "devDependencies": { + "@algorandfoundation/algokit-utils": "^7.0.0-beta.7", "@algorandfoundation/algorand-typescript": "file:packages/algo-ts/dist", "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", diff --git a/src/util/generate-temp-file.ts b/src/util/generate-temp-file.ts index cc04b063..cd3b0a8c 100644 --- a/src/util/generate-temp-file.ts +++ b/src/util/generate-temp-file.ts @@ -1,8 +1,10 @@ import { randomUUID } from 'crypto' import fs from 'fs' +import { globIterateSync } from 'glob' import type { WriteFileOptions } from 'node:fs' -import path from 'node:path' import os from 'os' +import upath from 'upath' +import { mkDirIfNotExists } from './index' export type TempFile = { writeFileSync(data: NodeJS.ArrayBufferView, options?: WriteFileOptions): void @@ -11,14 +13,14 @@ export type TempFile = { } & Disposable function ensureTempDir(): string { - const tempDir = path.join(os.tmpdir(), 'puya-ts') - if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir) + const tempDir = upath.join(os.tmpdir(), 'puya-ts') + mkDirIfNotExists(tempDir) return tempDir } export function generateTempFile(options?: { ext?: string }): TempFile { const { ext = 'tmp' } = options ?? {} - const filePath = path.join(ensureTempDir(), `${randomUUID()}.${ext}`) + const filePath = upath.join(ensureTempDir(), `${randomUUID()}.${ext}`) return { get filePath() { @@ -32,3 +34,28 @@ export function generateTempFile(options?: { ext?: string }): TempFile { }, } } +export type TempDir = { + readonly dirPath: string + files(): IterableIterator +} & Disposable + +export function generateTempDir(): TempDir { + const dirPath = upath.join(ensureTempDir(), `${randomUUID()}`) + mkDirIfNotExists(dirPath) + + return { + get dirPath() { + return dirPath + }, + *files(): IterableIterator { + for (const p of globIterateSync(upath.join(dirPath, '**'), { + nodir: true, + })) { + yield p + } + }, + [Symbol.dispose]() { + fs.rmSync(dirPath, { recursive: true, force: true }) + }, + } +} diff --git a/tests/onchain/abi-decorators.spec.ts b/tests/onchain/abi-decorators.spec.ts new file mode 100644 index 00000000..79e5714c --- /dev/null +++ b/tests/onchain/abi-decorators.spec.ts @@ -0,0 +1,15 @@ +import { describe } from 'vitest' +import { createTestFixture } from './util/test-fixture' + +describe('abi-decorators', () => { + const test = createTestFixture('tests/approvals/abi-decorators.algo.ts', { createParams: { method: 'createMethod' } }) + test('can be created', async ({ appFactory }) => { + await appFactory.send.create({ method: 'createMethod' }) + }) + test('methods can be called', async ({ appClient, expect }) => { + await appClient.send.call({ method: 'justNoop' }) + await appClient.send.call({ method: 'allActions', onComplete: 1 }) + const { return: returnValue } = await appClient.send.call({ method: 'overrideReadonlyName' }) + expect(returnValue).toBe(5n) + }) +}) diff --git a/tests/onchain/accounts.spec.ts b/tests/onchain/accounts.spec.ts new file mode 100644 index 00000000..71962474 --- /dev/null +++ b/tests/onchain/accounts.spec.ts @@ -0,0 +1,32 @@ +import { describe } from 'vitest' +import { createTestFixture } from './util/test-fixture' + +describe('accounts', () => { + const test = createTestFixture('tests/approvals/accounts.algo.ts') + + test('returns account data', async ({ appClient, expect, assetFactory, testAccount }) => { + const asset = await assetFactory({ assetName: 'Asset 1', sender: testAccount.addr, total: 1n }) + + const result = await appClient.send.call({ method: 'getAccountInfo', args: [testAccount.addr, asset] }) + + const returnValue = result.return as { + bytes: number[] + balance: bigint + minBalance: bigint + authAddress: number[] + totalNumUint: bigint + totalNumByteSlice: bigint + totalExtraAppPages: bigint + totalAppsCreated: bigint + totalAppsOptedIn: bigint + totalAssetsCreated: bigint + totalAssets: bigint + totalBoxes: bigint + totalBoxBytes: bigint + isOptInApp: boolean + isOptInAsset: boolean + } + + expect(returnValue.authAddress).toStrictEqual(new Array(32).fill(0)) + }) +}) diff --git a/tests/onchain/util/test-fixture.ts b/tests/onchain/util/test-fixture.ts new file mode 100644 index 00000000..9b85b0ad --- /dev/null +++ b/tests/onchain/util/test-fixture.ts @@ -0,0 +1,93 @@ +import { microAlgos } from '@algorandfoundation/algokit-utils' +import { algorandFixture } from '@algorandfoundation/algokit-utils/testing' +import type { AppClient } from '@algorandfoundation/algokit-utils/types/app-client' +import type { AppFactory, AppFactoryDeployParams } from '@algorandfoundation/algokit-utils/types/app-factory' +import type { AppSpec } from '@algorandfoundation/algokit-utils/types/app-spec' +import type { AssetCreateParams } from '@algorandfoundation/algokit-utils/types/composer' +import type { AlgorandFixture } from '@algorandfoundation/algokit-utils/types/testing' +import fs from 'fs' +import { test } from 'vitest' +import { compile } from '../../../src' +import { buildCompileOptions } from '../../../src/compile-options' +import { LoggingContext, LogLevel } from '../../../src/logger' +import { defaultPuyaOptions } from '../../../src/puya/options' +import { generateTempDir } from '../../../src/util/generate-temp-file' + +export function createTestFixture(path: string, defaultDeployParams?: AppFactoryDeployParams) { + const _localnet = algorandFixture({ + testAccountFunding: microAlgos(100_000_000_000), + }) + + return test.extend<{ + appFactory: AppFactory + appClient: AppClient + localnet: AlgorandFixture + testAccount: typeof _localnet.context.testAccount + assetFactory: (assetCreateParams: AssetCreateParams) => Promise + }>({ + localnet: async ({ expect }, use) => { + await _localnet.beforeEach() + await use(_localnet) + }, + appFactory: async ({ expect, localnet }, use) => { + const { appSpecs } = compilePath(path) + if (appSpecs.length === 0) { + expect.fail(`${path} does not contain any ARC4 contracts`) + } + if (appSpecs.length > 1) { + expect.fail(`${path} contains multiple ARC4 contracts, please specify a contract name in the options`) + } + const appFactory = localnet.algorand.client.getAppFactory({ defaultSender: localnet.context.testAccount.addr, appSpec: appSpecs[0] }) + await use(appFactory) + }, + appClient: async ({ appFactory }, use) => { + const { appClient } = await appFactory.deploy(defaultDeployParams ?? {}) + await use(appClient) + }, + testAccount: async ({ localnet }, use) => { + await use(localnet.context.testAccount) + }, + assetFactory: async ({ localnet }, use) => { + use(async (assetCreateParams: AssetCreateParams) => { + const { assetId } = await localnet.algorand.send.assetCreate(assetCreateParams) + return assetId + }) + }, + }) +} + +type CompilationArtifacts = { + appSpecs: AppSpec[] +} + +function compilePath(path: string): CompilationArtifacts { + using tempDir = generateTempDir() + using logCtx = LoggingContext.create() + + compile( + buildCompileOptions({ + outputAwstJson: false, + outputAwst: false, + paths: [path], + outDir: tempDir.dirPath, + dryRun: false, + logLevel: LogLevel.Error, + }), + { + ...defaultPuyaOptions, + outputTeal: false, + outputArc32: true, + }, + ) + const appSpecs = new Array() + for (const filePath of tempDir.files()) { + if (filePath.endsWith('.arc32.json')) { + appSpecs.push(JSON.parse(fs.readFileSync(filePath, 'utf-8'))) + } + } + + //TODO: check log context for errors + return { + appSpecs, + } +}