Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tests for auction and voting example contracts #22

Merged
merged 2 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions examples/auction/contract.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
7 changes: 2 additions & 5 deletions examples/hello-world-abi/contract.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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)
Expand All @@ -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])
})
})
4 changes: 3 additions & 1 deletion examples/htlc-logicsig/signature.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
9 changes: 5 additions & 4 deletions examples/precompiled/contract.spec.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion examples/voting/contract.algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export class VotingRoundApp extends arc4.Contract {
note: note,
fee: Global.minTxnFee,
})
.submit().configAsset
.submit().createdAsset
}

@abimethod({ readonly: true })
Expand Down
96 changes: 96 additions & 0 deletions examples/voting/contract.spec.ts
Original file line number Diff line number Diff line change
@@ -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<UintN8>(
...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<UintN8>(
...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')
})
})
13 changes: 10 additions & 3 deletions src/decode-logs.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
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'

export type DecodedLog<T extends LogDecoding> = T extends 'i' ? bigint : T extends 's' ? string : Uint8Array
export type DecodedLogs<T extends [...LogDecoding[]]> = {
[Index in keyof T]: DecodedLog<T[Index]>
} & { length: T['length'] }

const ABI_RETURN_VALUE_LOG_PREFIX_LENGTH = asNumber(ABI_RETURN_VALUE_LOG_PREFIX.length)
export function decodeLogs<const T extends [...LogDecoding[]]>(logs: bytes[], decoding: T): DecodedLogs<T> {
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<T>
}
Loading
Loading