Skip to content

Commit

Permalink
Merge pull request #22 from algorandfoundation/feat-examples
Browse files Browse the repository at this point in the history
feat: add tests for auction and voting example contracts
  • Loading branch information
boblat authored Dec 31, 2024
2 parents 965d07c + 3b2a05d commit e9abc14
Show file tree
Hide file tree
Showing 23 changed files with 381 additions and 101 deletions.
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

0 comments on commit e9abc14

Please sign in to comment.