-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22 from algorandfoundation/feat-examples
feat: add tests for auction and voting example contracts
- Loading branch information
Showing
23 changed files
with
381 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
Oops, something went wrong.