diff --git a/examples/auction/contract.spec.ts b/examples/auction/contract.spec.ts new file mode 100644 index 0000000..f1cc2e5 --- /dev/null +++ b/examples/auction/contract.spec.ts @@ -0,0 +1,160 @@ +import { TransactionType } from '@algorandfoundation/algorand-typescript' +import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing' +import { afterEach, describe, expect, it } from 'vitest' +import { Auction } from './contract.algo' + +describe('Auction', () => { + const ctx = new TestExecutionContext() + afterEach(() => { + ctx.reset() + }) + + it('should be able to opt into an asset', () => { + // Arrange + const asset = ctx.any.asset() + const contract = ctx.contract.create(Auction) + contract.createApplication() + + // Act + contract.optIntoAsset(asset) + + // Assert + expect(contract.asa.value.id).toEqual(asset.id) + const innerTxn = ctx.txn.lastGroup.lastItxnGroup().getAssetTransferInnerTxn() + + expect(innerTxn.assetReceiver, 'Asset receiver does not match').toEqual(ctx.ledger.getApplicationForContract(contract).address) + + expect(innerTxn.xferAsset, 'Transferred asset does not match').toEqual(asset) + }) + + it('should be able to start an auction', () => { + // Arrange + const contract = ctx.contract.create(Auction) + contract.createApplication() + + const app = ctx.ledger.getApplicationForContract(contract) + + const latestTimestamp = ctx.any.uint64(1, 1000) + const startingPrice = ctx.any.uint64() + const auctionDuration = ctx.any.uint64(100, 1000) + const axferTxn = ctx.any.txn.assetTransfer({ + assetReceiver: app.address, + assetAmount: startingPrice, + }) + contract.asaAmt.value = startingPrice + ctx.ledger.patchGlobalData({ + latestTimestamp: latestTimestamp, + }) + + // Act + contract.startAuction(startingPrice, auctionDuration, axferTxn) + + // Assert + expect(contract.auctionEnd.value).toEqual(latestTimestamp + auctionDuration) + expect(contract.previousBid.value).toEqual(startingPrice) + expect(contract.asaAmt.value).toEqual(startingPrice) + }) + + it('should be able to bid', () => { + // Arrange + const account = ctx.defaultSender + const auctionEnd = ctx.any.uint64(Date.now() + 10_000) + const previousBid = ctx.any.uint64(1, 100) + const payAmount = ctx.any.uint64() + + const contract = ctx.contract.create(Auction) + contract.createApplication() + contract.auctionEnd.value = auctionEnd + contract.previousBid.value = previousBid + const pay = ctx.any.txn.payment({ sender: account, amount: payAmount }) + + // Act + contract.bid(pay) + + // Assert + expect(contract.previousBid.value).toEqual(payAmount) + expect(contract.previousBidder.value).toEqual(account) + expect(contract.claimableAmount(account).value).toEqual(payAmount) + }) + + it('should be able to claim bids', () => { + // Arrange + const account = ctx.any.account() + const contract = ctx.contract.create(Auction) + contract.createApplication() + + const claimableAmount = ctx.any.uint64() + contract.claimableAmount(account).value = claimableAmount + + contract.previousBidder.value = account + const previousBid = ctx.any.uint64(undefined, claimableAmount) + contract.previousBid.value = previousBid + + // Act + ctx.txn.createScope([ctx.any.txn.applicationCall({ sender: account })]).execute(() => { + contract.claimBids() + }) + + // Assert + const expectedPayment = claimableAmount - previousBid + const lastInnerTxn = ctx.txn.lastGroup.lastItxnGroup().getPaymentInnerTxn() + + expect(lastInnerTxn.amount).toEqual(expectedPayment) + expect(lastInnerTxn.receiver).toEqual(account) + expect(contract.claimableAmount(account).value).toEqual(claimableAmount - expectedPayment) + }) + + it('should be able to claim asset', () => { + // Arrange + ctx.ledger.patchGlobalData({ latestTimestamp: ctx.any.uint64() }) + const contract = ctx.contract.create(Auction) + contract.createApplication() + + contract.auctionEnd.value = ctx.any.uint64(1, 100) + contract.previousBidder.value = ctx.defaultSender + const asaAmount = ctx.any.uint64(1000, 2000) + contract.asaAmt.value = asaAmount + const asset = ctx.any.asset() + + // Act + contract.claimAsset(asset) + + // Assert + const lastInnerTxn = ctx.txn.lastGroup.lastItxnGroup().getAssetTransferInnerTxn() + expect(lastInnerTxn.xferAsset).toEqual(asset) + expect(lastInnerTxn.assetCloseTo).toEqual(ctx.defaultSender) + expect(lastInnerTxn.assetReceiver).toEqual(ctx.defaultSender) + expect(lastInnerTxn.assetAmount).toEqual(asaAmount) + }) + + it('should be able to delete application', () => { + // Arrange + const account = ctx.any.account() + + // Act + // setting sender will determine creator + let contract + ctx.txn.createScope([ctx.any.txn.applicationCall({ sender: account })]).execute(() => { + contract = ctx.contract.create(Auction) + contract.createApplication() + }) + + ctx.txn.createScope([ctx.any.txn.applicationCall({ onCompletion: 'DeleteApplication' })]).execute(() => { + contract!.deleteApplication() + }) + + // Assert + const innerTransactions = ctx.txn.lastGroup.lastItxnGroup().getPaymentInnerTxn() + expect(innerTransactions).toBeTruthy() + expect(innerTransactions.type).toEqual(TransactionType.Payment) + expect(innerTransactions.receiver).toEqual(account) + expect(innerTransactions.closeRemainderTo).toEqual(account) + }) + + it('should be able to call clear state program', () => { + const contract = ctx.contract.create(Auction) + contract.createApplication() + + expect(contract.clearStateProgram()).toBeTruthy() + }) +}) diff --git a/examples/hello-world-abi/contract.spec.ts b/examples/hello-world-abi/contract.spec.ts index 55e85b9..32de4fe 100644 --- a/examples/hello-world-abi/contract.spec.ts +++ b/examples/hello-world-abi/contract.spec.ts @@ -1,7 +1,6 @@ import { assert, Bytes, TransactionType } 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 HelloWorldContract from './contract.algo' describe('HelloWorldContract', () => { @@ -16,9 +15,8 @@ describe('HelloWorldContract', () => { expect(result).toBe('Bananas') const bananasBytes = Bytes('Bananas') - const abiLog = ABI_RETURN_VALUE_LOG_PREFIX.concat(bananasBytes) const logs = ctx.exportLogs(ctx.txn.lastActive.appId.id, 's', 'b') - expect(logs).toStrictEqual([result, abiLog]) + expect(logs).toStrictEqual([result, bananasBytes]) }) it('logs the returned value when sayHello is called', async () => { const contract = ctx.contract.create(HelloWorldContract) @@ -27,8 +25,7 @@ describe('HelloWorldContract', () => { expect(result).toBe('Hello John Doe') const helloBytes = Bytes('Hello John Doe') - const abiLog = ABI_RETURN_VALUE_LOG_PREFIX.concat(helloBytes) const logs = ctx.exportLogs(ctx.txn.lastActive.appId.id, 's', 'b') - expect(logs).toStrictEqual([result, abiLog]) + expect(logs).toStrictEqual([result, helloBytes]) }) }) diff --git a/examples/htlc-logicsig/signature.spec.ts b/examples/htlc-logicsig/signature.spec.ts index 2224f10..c9cab8f 100644 --- a/examples/htlc-logicsig/signature.spec.ts +++ b/examples/htlc-logicsig/signature.spec.ts @@ -2,9 +2,11 @@ import { Account, Bytes } from '@algorandfoundation/algorand-typescript' import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing' import algosdk from 'algosdk' import { afterEach, describe, expect, it } from 'vitest' -import { ZERO_ADDRESS } from '../../src/constants' import HashedTimeLockedLogicSig from './signature.algo' +const ZERO_ADDRESS_B32 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ' +const ZERO_ADDRESS = Bytes.fromBase32(ZERO_ADDRESS_B32) + describe('HTLC LogicSig', () => { const ctx = new TestExecutionContext() diff --git a/examples/precompiled/contract.spec.ts b/examples/precompiled/contract.spec.ts index 5f85484..88cf330 100644 --- a/examples/precompiled/contract.spec.ts +++ b/examples/precompiled/contract.spec.ts @@ -1,11 +1,12 @@ -import { arc4 } from '@algorandfoundation/algorand-typescript' +import { arc4, Bytes } from '@algorandfoundation/algorand-typescript' import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing' import { afterEach, describe, it } from 'vitest' -import { ABI_RETURN_VALUE_LOG_PREFIX, MAX_BYTES_SIZE } from '../../src/constants' -import { asUint64Cls } from '../../src/util' import { HelloFactory } from './contract.algo' import { Hello, HelloTemplate, HelloTemplateCustomPrefix, LargeProgram, TerribleCustodialAccount } from './precompiled-apps.algo' +const MAX_BYTES_SIZE = 4096 +const ABI_RETURN_VALUE_LOG_PREFIX = Bytes.fromHex('151F7C75') + describe('pre compiled app calls', () => { const ctx = new TestExecutionContext() afterEach(() => { @@ -58,7 +59,7 @@ describe('pre compiled app calls', () => { // Arrange const largeProgramApp = ctx.any.application({ approvalProgram: ctx.any.bytes(20), - appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(asUint64Cls(MAX_BYTES_SIZE).toBytes().asAlgoTs())], + appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(Bytes(MAX_BYTES_SIZE))], }) ctx.setCompiledApp(LargeProgram, largeProgramApp.id) diff --git a/examples/voting/contract.algo.ts b/examples/voting/contract.algo.ts index 53a43e3..734f50d 100644 --- a/examples/voting/contract.algo.ts +++ b/examples/voting/contract.algo.ts @@ -154,7 +154,7 @@ export class VotingRoundApp extends arc4.Contract { note: note, fee: Global.minTxnFee, }) - .submit().configAsset + .submit().createdAsset } @abimethod({ readonly: true }) diff --git a/examples/voting/contract.spec.ts b/examples/voting/contract.spec.ts new file mode 100644 index 0000000..c1c59c8 --- /dev/null +++ b/examples/voting/contract.spec.ts @@ -0,0 +1,96 @@ +import { Bytes, Uint64 } from '@algorandfoundation/algorand-typescript' +import { bytesToUint8Array, TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing' +import { DynamicArray, UintN8 } from '@algorandfoundation/algorand-typescript/arc4' +import nacl from 'tweetnacl' +import { afterEach, describe, expect, it } from 'vitest' +import { VotingRoundApp } from './contract.algo' + +describe('VotingRoundApp', () => { + const ctx = new TestExecutionContext() + + const boostrapMinBalanceReq = Uint64(287100) + const voteMinBalanceReq = Uint64(21300) + const tallyBoxSize = 208 + const keyPair = nacl.sign.keyPair() + const voteId = ctx.any.string() + + const createContract = () => { + const contract = ctx.contract.create(VotingRoundApp) + const snapshotPublicKey = Bytes(keyPair.publicKey) + const metadataIpfsCid = ctx.any.string(16) + const startTime = ctx.any.uint64(Date.now() - 10_000, Date.now()) + const endTime = ctx.any.uint64(Date.now() + 10_000, Date.now() + 100_000) + const optionCounts = new DynamicArray( + ...Array(13) + .fill(0) + .map(() => new UintN8(2)), + ) + const quorum = ctx.any.uint64() + const nftImageUrl = ctx.any.string(64) + contract.create(voteId, snapshotPublicKey, metadataIpfsCid, startTime, endTime, optionCounts, quorum, nftImageUrl) + return contract + } + + afterEach(() => { + ctx.reset() + }) + + it('shoulld be able to bootstrap', () => { + const contract = createContract() + const app = ctx.ledger.getApplicationForContract(contract) + contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq })) + + expect(contract.isBootstrapped.value).toEqual(true) + expect(contract.tallyBox.value).toEqual(Bytes.fromHex('00'.repeat(tallyBoxSize))) + }) + + it('should be able to get pre conditions', () => { + const contract = createContract() + const app = ctx.ledger.getApplicationForContract(contract) + contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq })) + + const account = ctx.any.account() + const signature = nacl.sign.detached(bytesToUint8Array(account.bytes), keyPair.secretKey) + ctx.txn.createScope([ctx.any.txn.applicationCall({ sender: account })]).execute(() => { + const preconditions = contract.getPreconditions(Bytes(signature)) + + expect(preconditions.is_allowed_to_vote).toEqual(1) + expect(preconditions.is_voting_open).toEqual(1) + expect(preconditions.has_already_voted).toEqual(0) + expect(preconditions.current_time).toEqual(ctx.txn.activeGroup.latestTimestamp) + }) + }) + + it('should be able to vote', () => { + const contract = createContract() + const app = ctx.ledger.getApplicationForContract(contract) + contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq })) + + const account = ctx.any.account() + const signature = nacl.sign.detached(bytesToUint8Array(account.bytes), keyPair.secretKey) + const answerIds = new DynamicArray( + ...Array(13) + .fill(0) + .map(() => new UintN8(Math.ceil(Math.random() * 10) % 2)), + ) + + ctx.txn.createScope([ctx.any.txn.applicationCall({ appId: app, sender: account })]).execute(() => { + contract.vote(ctx.any.txn.payment({ receiver: app.address, amount: voteMinBalanceReq }), Bytes(signature), answerIds) + + expect(contract.votesByAccount.get(account).bytes).toEqual(answerIds.bytes) + expect(contract.voterCount.value).toEqual(13) + }) + }) + + it('should be able to close', () => { + const contract = createContract() + const app = ctx.ledger.getApplicationForContract(contract) + contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq })) + + contract.close() + + expect(contract.closeTime.value).toEqual(ctx.txn.lastGroup.latestTimestamp) + expect(contract.nftAsset.value.name).toEqual(`[VOTE RESULT] ${voteId}`) + expect(contract.nftAsset.value.unitName).toEqual('VOTERSLT') + }) +}) diff --git a/src/decode-logs.ts b/src/decode-logs.ts index 2fa74f0..e020a27 100644 --- a/src/decode-logs.ts +++ b/src/decode-logs.ts @@ -1,4 +1,6 @@ import { bytes, op } from '@algorandfoundation/algorand-typescript' +import { ABI_RETURN_VALUE_LOG_PREFIX } from './constants' +import { asNumber } from './util' export type LogDecoding = 'i' | 's' | 'b' @@ -6,15 +8,20 @@ export type DecodedLog = T extends 'i' ? bigint : T exten export type DecodedLogs = { [Index in keyof T]: DecodedLog } & { length: T['length'] } + +const ABI_RETURN_VALUE_LOG_PREFIX_LENGTH = asNumber(ABI_RETURN_VALUE_LOG_PREFIX.length) export function decodeLogs(logs: bytes[], decoding: T): DecodedLogs { return logs.map((log, i) => { + const value = log.slice(0, ABI_RETURN_VALUE_LOG_PREFIX_LENGTH).equals(ABI_RETURN_VALUE_LOG_PREFIX) + ? log.slice(ABI_RETURN_VALUE_LOG_PREFIX_LENGTH) + : log switch (decoding[i]) { case 'i': - return op.btoi(log) + return op.btoi(value) case 's': - return log.toString() + return value.toString() default: - return log + return value } }) as DecodedLogs } diff --git a/src/encoders.ts b/src/encoders.ts index be9c12c..2cf964c 100644 --- a/src/encoders.ts +++ b/src/encoders.ts @@ -1,11 +1,12 @@ import { biguint, BigUint, bytes, internal, TransactionType, uint64, Uint64 } from '@algorandfoundation/algorand-typescript' -import { OnCompleteAction } from '@algorandfoundation/algorand-typescript/arc4' +import { ARC4Encoded, OnCompleteAction } from '@algorandfoundation/algorand-typescript/arc4' import { AccountCls } from './impl/account' import { ApplicationCls } from './impl/application' import { AssetCls } from './impl/asset' -import { arc4Encoders, getArc4Encoder } from './impl/encoded-types' +import { BytesBackedCls, Uint64BackedCls } from './impl/base' +import { arc4Encoders, encodeArc4Impl, getArc4Encoder } from './impl/encoded-types' import { DeliberateAny } from './typescript-helpers' -import { asBytes, asUint8Array } from './util' +import { asBytes, asMaybeBigUintCls, asMaybeBytesCls, asMaybeUint64Cls, asUint64Cls, asUint8Array, nameOfType } from './util' export type TypeInfo = { name: string @@ -59,3 +60,31 @@ export const encoders: Record> = { export const getEncoder = (typeInfo: TypeInfo): fromBytes => { return getArc4Encoder(typeInfo, encoders) } + +export const toBytes = (val: unknown): bytes => { + const uint64Val = asMaybeUint64Cls(val) + if (uint64Val !== undefined) { + return uint64Val.toBytes().asAlgoTs() + } + const bytesVal = asMaybeBytesCls(val) + if (bytesVal !== undefined) { + return bytesVal.asAlgoTs() + } + const bigUintVal = asMaybeBigUintCls(val) + if (bigUintVal !== undefined) { + return bigUintVal.toBytes().asAlgoTs() + } + if (val instanceof BytesBackedCls) { + return val.bytes + } + if (val instanceof Uint64BackedCls) { + return asUint64Cls(val.uint64).toBytes().asAlgoTs() + } + if (val instanceof ARC4Encoded) { + return val.bytes + } + if (Array.isArray(val) || typeof val === 'object') { + return encodeArc4Impl('', val) + } + internal.errors.internalError(`Invalid type for bytes: ${nameOfType(val)}`) +} diff --git a/src/impl/app-global.ts b/src/impl/app-global.ts index eaa5470..6d04bb7 100644 --- a/src/impl/app-global.ts +++ b/src/impl/app-global.ts @@ -1,6 +1,7 @@ import { Application, Bytes, bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript' import { lazyContext } from '../context-helpers/internal-context' -import { asBytes, toBytes } from '../util' +import { toBytes } from '../encoders' +import { asBytes } from '../util' import { getApp } from './app-params' export const AppGlobal: internal.opTypes.AppGlobalType = { diff --git a/src/impl/app-local.ts b/src/impl/app-local.ts index d6feb16..c8329f6 100644 --- a/src/impl/app-local.ts +++ b/src/impl/app-local.ts @@ -1,6 +1,7 @@ import { Account, Application, Bytes, bytes, internal, Uint64, uint64 } from '@algorandfoundation/algorand-typescript' import { lazyContext } from '../context-helpers/internal-context' -import { asBytes, toBytes } from '../util' +import { toBytes } from '../encoders' +import { asBytes } from '../util' import { getAccount } from './acct-params' import { getApp } from './app-params' diff --git a/src/impl/box.ts b/src/impl/box.ts index c1bdf84..a7ffabe 100644 --- a/src/impl/box.ts +++ b/src/impl/box.ts @@ -1,7 +1,8 @@ import { bytes, internal, uint64 } from '@algorandfoundation/algorand-typescript' import { MAX_BOX_SIZE } from '../constants' import { lazyContext } from '../context-helpers/internal-context' -import { asBytes, asBytesCls, asNumber, asUint8Array, conactUint8Arrays, toBytes } from '../util' +import { toBytes } from '../encoders' +import { asBytes, asBytesCls, asNumber, asUint8Array, conactUint8Arrays } from '../util' export const Box: internal.opTypes.BoxType = { create(a: internal.primitives.StubBytesCompat, b: internal.primitives.StubUint64Compat): boolean { diff --git a/src/impl/inner-transactions.ts b/src/impl/inner-transactions.ts index b2ec556..7ebe59f 100644 --- a/src/impl/inner-transactions.ts +++ b/src/impl/inner-transactions.ts @@ -1,7 +1,7 @@ import { Account, Application, arc4, Asset, bytes, internal, itxn, TransactionType, uint64 } from '@algorandfoundation/algorand-typescript' import { lazyContext } from '../context-helpers/internal-context' import { Mutable } from '../typescript-helpers' -import { asBytes } from '../util' +import { asBytes, asNumber } from '../util' import { getApp } from './app-params' import { getAsset } from './asset-params' import { InnerTxn, InnerTxnFields } from './itxn' @@ -66,12 +66,15 @@ export class AssetConfigInnerTxn extends AssetConfigTransaction implements itxn. /* @internal */ constructor(fields: itxn.AssetConfigFields) { const { assetName, unitName, url, ...rest } = mapCommonFields(fields) - const createdAsset = lazyContext.any.asset({ - name: typeof assetName === 'string' ? asBytes(assetName) : assetName, - unitName: typeof unitName === 'string' ? asBytes(unitName) : unitName, - url: typeof url === 'string' ? asBytes(url) : url, - ...rest, - }) + const createdAsset = + !rest.configAsset || !asNumber(rest.configAsset.id) + ? lazyContext.any.asset({ + name: typeof assetName === 'string' ? asBytes(assetName) : assetName, + unitName: typeof unitName === 'string' ? asBytes(unitName) : unitName, + url: typeof url === 'string' ? asBytes(url) : url, + ...rest, + }) + : undefined super({ assetName: typeof assetName === 'string' ? asBytes(assetName) : assetName, @@ -191,27 +194,21 @@ export function submitGroup(...transactionFie return transactionFields.map((f: (typeof transactionFields)[number]) => f.submit()) as itxn.TxnFor } export function payment(fields: itxn.PaymentFields): itxn.PaymentItxnParams { - ensureItxnGroupBegin() return new ItxnParams(fields, TransactionType.Payment) } export function keyRegistration(fields: itxn.KeyRegistrationFields): itxn.KeyRegistrationItxnParams { - ensureItxnGroupBegin() return new ItxnParams(fields, TransactionType.KeyRegistration) } export function assetConfig(fields: itxn.AssetConfigFields): itxn.AssetConfigItxnParams { - ensureItxnGroupBegin() return new ItxnParams(fields, TransactionType.AssetConfig) } export function assetTransfer(fields: itxn.AssetTransferFields): itxn.AssetTransferItxnParams { - ensureItxnGroupBegin() return new ItxnParams(fields, TransactionType.AssetTransfer) } export function assetFreeze(fields: itxn.AssetFreezeFields): itxn.AssetFreezeItxnParams { - ensureItxnGroupBegin() return new ItxnParams(fields, TransactionType.AssetFreeze) } export function applicationCall(fields: itxn.ApplicationCallFields): itxn.ApplicationCallItxnParams { - ensureItxnGroupBegin() return new ItxnParams(fields, TransactionType.ApplicationCall) } @@ -221,7 +218,9 @@ export class ItxnParams(this.#fields) as unknown as TTransaction + const innerTxn = createInnerTxn(this.#fields) as unknown as TTransaction + lazyContext.txn.activeGroup.addInnerTransactionGroup(innerTxn) + return innerTxn } set(p: Partial) { @@ -232,9 +231,3 @@ export class ItxnParams(this.#fields, this.#fields.type) } } - -const ensureItxnGroupBegin = () => { - if (!lazyContext.activeGroup.constructingItxnGroup.length) { - lazyContext.activeGroup.beginInnerTransactionGroup() - } -} diff --git a/src/impl/match.ts b/src/impl/match.ts index da87e3f..2ef59c6 100644 --- a/src/impl/match.ts +++ b/src/impl/match.ts @@ -33,11 +33,11 @@ export const matchImpl: typeof match = (subject, test): boolean => { getBigIntValue(subject.uint64 as unknown as internal.primitives.Uint64Cls) === getBigIntValue((test as unknown as Uint64BackedCls).uint64 as unknown as internal.primitives.Uint64Cls) ) - } else if (subject instanceof ARC4Encoded) { - return subject.bytes.equals((test as unknown as ARC4Encoded).bytes) - } else if (Array.isArray(subject)) { - return (test as []).map((x, i) => matchImpl((subject as DeliberateAny)[i], x as DeliberateAny)).every((x) => x) - } else if (typeof subject === 'object') { + } else if (test instanceof ARC4Encoded) { + return (subject as unknown as ARC4Encoded).bytes.equals(test.bytes) + } else if (Array.isArray(test)) { + return test.map((x, i) => matchImpl((subject as DeliberateAny)[i], x as DeliberateAny)).every((x) => x) + } else if (typeof test === 'object') { return Object.entries(test!) .map(([k, v]) => matchImpl((subject as DeliberateAny)[k], v as DeliberateAny)) .every((x) => x) diff --git a/src/impl/state.ts b/src/impl/state.ts index 2382644..2f14e79 100644 --- a/src/impl/state.ts +++ b/src/impl/state.ts @@ -17,9 +17,9 @@ import { import { AccountMap } from '../collections/custom-key-map' import { MAX_BOX_SIZE } from '../constants' import { lazyContext } from '../context-helpers/internal-context' -import { getEncoder, TypeInfo } from '../encoders' +import { getEncoder, toBytes, TypeInfo } from '../encoders' import { getGenericTypeInfo } from '../runtime-helpers' -import { asBytes, asBytesCls, asNumber, asUint8Array, conactUint8Arrays, toBytes } from '../util' +import { asBytes, asBytesCls, asNumber, asUint8Array, conactUint8Arrays } from '../util' export class GlobalStateCls { private readonly _type: string = GlobalStateCls.name diff --git a/src/impl/transactions.ts b/src/impl/transactions.ts index fda0abd..d2d7573 100644 --- a/src/impl/transactions.ts +++ b/src/impl/transactions.ts @@ -13,17 +13,9 @@ import { } from '@algorandfoundation/algorand-typescript' import { ABI_RETURN_VALUE_LOG_PREFIX, MAX_ITEMS_IN_LOG } from '../constants' import { lazyContext } from '../context-helpers/internal-context' +import { toBytes } from '../encoders' import { Mutable, ObjectKeys } from '../typescript-helpers' -import { - asBytes, - asMaybeBytesCls, - asMaybeUint64Cls, - asNumber, - asUint64Cls, - combineIntoMaxBytePages, - getRandomBytes, - toBytes, -} from '../util' +import { asBytes, asMaybeBytesCls, asMaybeUint64Cls, asNumber, asUint64Cls, combineIntoMaxBytePages, getRandomBytes } from '../util' const baseDefaultFields = () => ({ sender: lazyContext.defaultSender, diff --git a/src/index.ts b/src/index.ts index 9d9b0b8..26df64c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export { TestExecutionContext } from './test-execution-context' +export { asUint8Array as bytesToUint8Array } from './util' diff --git a/src/runtime-helpers.ts b/src/runtime-helpers.ts index 2edd1b5..3d7c5bf 100644 --- a/src/runtime-helpers.ts +++ b/src/runtime-helpers.ts @@ -3,6 +3,7 @@ import { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4' import { MAX_UINT64 } from './constants' import type { TypeInfo } from './encoders' import { AccountCls } from './impl/account' +import { Uint64BackedCls } from './impl/base' import { DeliberateAny } from './typescript-helpers' import { nameOfType } from './util' @@ -35,7 +36,7 @@ type UnaryOps = '~' function tryGetBigInt(value: unknown): bigint | undefined { if (typeof value == 'bigint') return value - if (typeof value == 'number') return BigInt(value) + if (typeof value == 'number' && Number.isInteger(value)) return BigInt(value) if (value instanceof internal.primitives.Uint64Cls) return value.valueOf() if (value instanceof internal.primitives.BigUintCls) return value.valueOf() return undefined @@ -48,6 +49,9 @@ export function binaryOp(left: unknown, right: unknown, op: BinaryOps) { if (left instanceof AccountCls && right instanceof AccountCls) { return accountBinaryOp(left, right, op) } + if (left instanceof Uint64BackedCls && right instanceof Uint64BackedCls) { + return uint64BackedClsBinaryOp(left, right, op) + } if (left instanceof internal.primitives.BigUintCls || right instanceof internal.primitives.BigUintCls) { return bigUintBinaryOp(left, right, op) } @@ -105,6 +109,15 @@ function accountBinaryOp(left: AccountCls, right: AccountCls, op: BinaryOps): De internal.errors.internalError(`Unsupported operator ${op}`) } } +function uint64BackedClsBinaryOp(left: Uint64BackedCls, right: Uint64BackedCls, op: BinaryOps): DeliberateAny { + switch (op) { + case '===': + case '!==': + return uint64BinaryOp(left.uint64, right.uint64, op) + default: + internal.errors.internalError(`Unsupported operator ${op}`) + } +} function uint64BinaryOp(left: DeliberateAny, right: DeliberateAny, op: BinaryOps): DeliberateAny { const lbi = internal.primitives.Uint64Cls.fromCompat(left).valueOf() const rbi = internal.primitives.Uint64Cls.fromCompat(right).valueOf() diff --git a/src/subcontexts/contract-context.ts b/src/subcontexts/contract-context.ts index 7f60248..4603e5b 100644 --- a/src/subcontexts/contract-context.ts +++ b/src/subcontexts/contract-context.ts @@ -21,7 +21,7 @@ import { import { BytesMap } from '../collections/custom-key-map' import { checkRoutingConditions } from '../context-helpers/context-util' import { lazyContext } from '../context-helpers/internal-context' -import type { TypeInfo } from '../encoders' +import { toBytes, type TypeInfo } from '../encoders' import { AccountCls } from '../impl/account' import { ApplicationCls } from '../impl/application' import { AssetCls } from '../impl/asset' @@ -37,7 +37,6 @@ import { } from '../impl/transactions' import { getGenericTypeInfo } from '../runtime-helpers' import { DeliberateAny, IConstructor } from '../typescript-helpers' -import { toBytes } from '../util' type ContractOptionsParameter = Parameters[0] diff --git a/src/subcontexts/transaction-context.ts b/src/subcontexts/transaction-context.ts index 04e1863..78ad364 100644 --- a/src/subcontexts/transaction-context.ts +++ b/src/subcontexts/transaction-context.ts @@ -88,14 +88,14 @@ export class TransactionContext { createScope(group: Array>, activeTransactionIndex?: number): ExecutionScope { const transactions = group.map((t) => (t instanceof DeferredAppCall ? t.txns : [t])).flat() const transactionGroup = new TransactionGroup(transactions, activeTransactionIndex) - const previousGroup = this.#activeGroup + this.#activeGroup = transactionGroup const scope = ScopeGenerator(() => { if (this.#activeGroup?.transactions?.length) { this.groups.push(this.#activeGroup) } - this.#activeGroup = previousGroup + this.#activeGroup = undefined }) return { execute: (body: () => TReturn) => { @@ -221,6 +221,9 @@ export class TransactionGroup { Object.assign(activeTransaction, filteredFields) } + addInnerTransactionGroup(...itxns: InnerTxn[]) { + this.itxnGroups.push(new ItxnGroup(itxns)) + } beginInnerTransactionGroup() { if (this.constructingItxnGroup.length) { internal.errors.internalError('itxn begin without itxn submit') @@ -252,6 +255,9 @@ export class TransactionGroup { this.constructingItxnGroup = [] } + lastItxnGroup() { + return this.getItxnGroup() + } getItxnGroup(index?: internal.primitives.StubUint64Compat): ItxnGroup { const i = index !== undefined ? asNumber(index) : undefined diff --git a/src/util.ts b/src/util.ts index ff0ec08..c139cd8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,4 @@ import { Account, Bytes, bytes, internal } from '@algorandfoundation/algorand-typescript' -import { ARC4Encoded } from '@algorandfoundation/algorand-typescript/arc4' import { encodingUtil } from '@algorandfoundation/puya-ts' import { randomBytes } from 'crypto' import { sha512_256 as js_sha512_256 } from 'js-sha512' @@ -15,7 +14,6 @@ import { MAX_UINT8, UINT512_SIZE, } from './constants' -import { BytesBackedCls, Uint64BackedCls } from './impl/base' import { DeliberateAny } from './typescript-helpers' export const nameOfType = (x: unknown) => { @@ -59,36 +57,6 @@ export const asBytes = (val: internal.primitives.StubBytesCompat | Uint8Array) = export const asUint8Array = (val: internal.primitives.StubBytesCompat | Uint8Array) => asBytesCls(val).asUint8Array() -export const toBytes = (val: unknown): bytes => { - const uint64Val = asMaybeUint64Cls(val) - if (uint64Val !== undefined) { - return uint64Val.toBytes().asAlgoTs() - } - const bytesVal = asMaybeBytesCls(val) - if (bytesVal !== undefined) { - return bytesVal.asAlgoTs() - } - const bigUintVal = asMaybeBigUintCls(val) - if (bigUintVal !== undefined) { - return bigUintVal.toBytes().asAlgoTs() - } - if (val instanceof BytesBackedCls) { - return val.bytes - } - if (val instanceof Uint64BackedCls) { - return asUint64Cls(val.uint64).toBytes().asAlgoTs() - } - if (Array.isArray(val)) { - return val.reduce((acc: bytes, cur: unknown) => { - return acc.concat(toBytes(cur)) - }, Bytes()) - } - if (val instanceof ARC4Encoded) { - return val.bytes - } - internal.errors.internalError(`Invalid type for bytes: ${nameOfType(val)}`) -} - export const asMaybeUint64Cls = (val: DeliberateAny) => { try { return internal.primitives.Uint64Cls.fromCompat(val) diff --git a/src/value-generators/avm.ts b/src/value-generators/avm.ts index a79ec08..a3cca0f 100644 --- a/src/value-generators/avm.ts +++ b/src/value-generators/avm.ts @@ -18,23 +18,34 @@ type AssetContextData = Partial & { assetId?: internal.primitives.Stu type ApplicationContextData = Partial & { applicationId?: internal.primitives.StubUint64Compat } export class AvmValueGenerator { - uint64(minValue: number | bigint = 0n, maxValue: number | bigint = MAX_UINT64): uint64 { - if (maxValue > MAX_UINT64) { + uint64(minValue: internal.primitives.StubUint64Compat = 0n, maxValue: internal.primitives.StubUint64Compat = MAX_UINT64): uint64 { + const min = asBigInt(minValue) + const max = asBigInt(maxValue) + if (max > MAX_UINT64) { internal.errors.internalError('maxValue must be less than or equal to MAX_UINT64') } - if (minValue > maxValue) { + if (min > max) { internal.errors.internalError('minValue must be less than or equal to maxValue') } - if (minValue < 0n || maxValue < 0n) { + if (min < 0n || max < 0n) { internal.errors.internalError('minValue and maxValue must be greater than or equal to 0') } - return Uint64(getRandomBigInt(minValue, maxValue)) + return Uint64(getRandomBigInt(min, max)) } bytes(length = MAX_BYTES_SIZE): bytes { return Bytes(new Uint8Array(randomBytes(length))) } + string(length = 11): string { + const setLength = 11 + return Array(Math.ceil(length / setLength)) + .fill(0) + .map(() => Math.random().toString(36).substring(2)) + .join('') + .substring(0, length) + } + account(input?: AccountContextData): Account { const account = input?.address ?? Account(getRandomBytes(32).asAlgoTs()) diff --git a/tests/references/box-map.spec.ts b/tests/references/box-map.spec.ts index 82bbed2..d6d379e 100644 --- a/tests/references/box-map.spec.ts +++ b/tests/references/box-map.spec.ts @@ -3,8 +3,9 @@ import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-te import { ARC4Encoded, DynamicArray, interpretAsArc4, Str, UintN64 } from '@algorandfoundation/algorand-typescript/arc4' import { afterEach, describe, expect, test } from 'vitest' import { MAX_UINT64 } from '../../src/constants' +import { toBytes } from '../../src/encoders' import { DeliberateAny } from '../../src/typescript-helpers' -import { asBytes, toBytes } from '../../src/util' +import { asBytes } from '../../src/util' const BOX_NOT_CREATED_ERROR = 'Box has not been created' diff --git a/tests/references/box.spec.ts b/tests/references/box.spec.ts index c64894a..9893344 100644 --- a/tests/references/box.spec.ts +++ b/tests/references/box.spec.ts @@ -3,8 +3,9 @@ import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-te import { ARC4Encoded, DynamicArray, interpretAsArc4, Str, UintN64 } from '@algorandfoundation/algorand-typescript/arc4' import { itob } from '@algorandfoundation/algorand-typescript/op' import { afterEach, describe, expect, it, test } from 'vitest' +import { toBytes } from '../../src/encoders' import { DeliberateAny } from '../../src/typescript-helpers' -import { asBytes, toBytes } from '../../src/util' +import { asBytes } from '../../src/util' import { BoxContract } from '../artifacts/box-contract/contract.algo' const BOX_NOT_CREATED_ERROR = 'Box has not been created'