Skip to content

Commit e9abc14

Browse files
authored
Merge pull request #22 from algorandfoundation/feat-examples
feat: add tests for auction and voting example contracts
2 parents 965d07c + 3b2a05d commit e9abc14

File tree

23 files changed

+381
-101
lines changed

23 files changed

+381
-101
lines changed

examples/auction/contract.spec.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { TransactionType } from '@algorandfoundation/algorand-typescript'
2+
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
3+
import { afterEach, describe, expect, it } from 'vitest'
4+
import { Auction } from './contract.algo'
5+
6+
describe('Auction', () => {
7+
const ctx = new TestExecutionContext()
8+
afterEach(() => {
9+
ctx.reset()
10+
})
11+
12+
it('should be able to opt into an asset', () => {
13+
// Arrange
14+
const asset = ctx.any.asset()
15+
const contract = ctx.contract.create(Auction)
16+
contract.createApplication()
17+
18+
// Act
19+
contract.optIntoAsset(asset)
20+
21+
// Assert
22+
expect(contract.asa.value.id).toEqual(asset.id)
23+
const innerTxn = ctx.txn.lastGroup.lastItxnGroup().getAssetTransferInnerTxn()
24+
25+
expect(innerTxn.assetReceiver, 'Asset receiver does not match').toEqual(ctx.ledger.getApplicationForContract(contract).address)
26+
27+
expect(innerTxn.xferAsset, 'Transferred asset does not match').toEqual(asset)
28+
})
29+
30+
it('should be able to start an auction', () => {
31+
// Arrange
32+
const contract = ctx.contract.create(Auction)
33+
contract.createApplication()
34+
35+
const app = ctx.ledger.getApplicationForContract(contract)
36+
37+
const latestTimestamp = ctx.any.uint64(1, 1000)
38+
const startingPrice = ctx.any.uint64()
39+
const auctionDuration = ctx.any.uint64(100, 1000)
40+
const axferTxn = ctx.any.txn.assetTransfer({
41+
assetReceiver: app.address,
42+
assetAmount: startingPrice,
43+
})
44+
contract.asaAmt.value = startingPrice
45+
ctx.ledger.patchGlobalData({
46+
latestTimestamp: latestTimestamp,
47+
})
48+
49+
// Act
50+
contract.startAuction(startingPrice, auctionDuration, axferTxn)
51+
52+
// Assert
53+
expect(contract.auctionEnd.value).toEqual(latestTimestamp + auctionDuration)
54+
expect(contract.previousBid.value).toEqual(startingPrice)
55+
expect(contract.asaAmt.value).toEqual(startingPrice)
56+
})
57+
58+
it('should be able to bid', () => {
59+
// Arrange
60+
const account = ctx.defaultSender
61+
const auctionEnd = ctx.any.uint64(Date.now() + 10_000)
62+
const previousBid = ctx.any.uint64(1, 100)
63+
const payAmount = ctx.any.uint64()
64+
65+
const contract = ctx.contract.create(Auction)
66+
contract.createApplication()
67+
contract.auctionEnd.value = auctionEnd
68+
contract.previousBid.value = previousBid
69+
const pay = ctx.any.txn.payment({ sender: account, amount: payAmount })
70+
71+
// Act
72+
contract.bid(pay)
73+
74+
// Assert
75+
expect(contract.previousBid.value).toEqual(payAmount)
76+
expect(contract.previousBidder.value).toEqual(account)
77+
expect(contract.claimableAmount(account).value).toEqual(payAmount)
78+
})
79+
80+
it('should be able to claim bids', () => {
81+
// Arrange
82+
const account = ctx.any.account()
83+
const contract = ctx.contract.create(Auction)
84+
contract.createApplication()
85+
86+
const claimableAmount = ctx.any.uint64()
87+
contract.claimableAmount(account).value = claimableAmount
88+
89+
contract.previousBidder.value = account
90+
const previousBid = ctx.any.uint64(undefined, claimableAmount)
91+
contract.previousBid.value = previousBid
92+
93+
// Act
94+
ctx.txn.createScope([ctx.any.txn.applicationCall({ sender: account })]).execute(() => {
95+
contract.claimBids()
96+
})
97+
98+
// Assert
99+
const expectedPayment = claimableAmount - previousBid
100+
const lastInnerTxn = ctx.txn.lastGroup.lastItxnGroup().getPaymentInnerTxn()
101+
102+
expect(lastInnerTxn.amount).toEqual(expectedPayment)
103+
expect(lastInnerTxn.receiver).toEqual(account)
104+
expect(contract.claimableAmount(account).value).toEqual(claimableAmount - expectedPayment)
105+
})
106+
107+
it('should be able to claim asset', () => {
108+
// Arrange
109+
ctx.ledger.patchGlobalData({ latestTimestamp: ctx.any.uint64() })
110+
const contract = ctx.contract.create(Auction)
111+
contract.createApplication()
112+
113+
contract.auctionEnd.value = ctx.any.uint64(1, 100)
114+
contract.previousBidder.value = ctx.defaultSender
115+
const asaAmount = ctx.any.uint64(1000, 2000)
116+
contract.asaAmt.value = asaAmount
117+
const asset = ctx.any.asset()
118+
119+
// Act
120+
contract.claimAsset(asset)
121+
122+
// Assert
123+
const lastInnerTxn = ctx.txn.lastGroup.lastItxnGroup().getAssetTransferInnerTxn()
124+
expect(lastInnerTxn.xferAsset).toEqual(asset)
125+
expect(lastInnerTxn.assetCloseTo).toEqual(ctx.defaultSender)
126+
expect(lastInnerTxn.assetReceiver).toEqual(ctx.defaultSender)
127+
expect(lastInnerTxn.assetAmount).toEqual(asaAmount)
128+
})
129+
130+
it('should be able to delete application', () => {
131+
// Arrange
132+
const account = ctx.any.account()
133+
134+
// Act
135+
// setting sender will determine creator
136+
let contract
137+
ctx.txn.createScope([ctx.any.txn.applicationCall({ sender: account })]).execute(() => {
138+
contract = ctx.contract.create(Auction)
139+
contract.createApplication()
140+
})
141+
142+
ctx.txn.createScope([ctx.any.txn.applicationCall({ onCompletion: 'DeleteApplication' })]).execute(() => {
143+
contract!.deleteApplication()
144+
})
145+
146+
// Assert
147+
const innerTransactions = ctx.txn.lastGroup.lastItxnGroup().getPaymentInnerTxn()
148+
expect(innerTransactions).toBeTruthy()
149+
expect(innerTransactions.type).toEqual(TransactionType.Payment)
150+
expect(innerTransactions.receiver).toEqual(account)
151+
expect(innerTransactions.closeRemainderTo).toEqual(account)
152+
})
153+
154+
it('should be able to call clear state program', () => {
155+
const contract = ctx.contract.create(Auction)
156+
contract.createApplication()
157+
158+
expect(contract.clearStateProgram()).toBeTruthy()
159+
})
160+
})

examples/hello-world-abi/contract.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { assert, Bytes, TransactionType } from '@algorandfoundation/algorand-typescript'
22
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
33
import { afterEach, describe, expect, it } from 'vitest'
4-
import { ABI_RETURN_VALUE_LOG_PREFIX } from '../../src/constants'
54
import HelloWorldContract from './contract.algo'
65

76
describe('HelloWorldContract', () => {
@@ -16,9 +15,8 @@ describe('HelloWorldContract', () => {
1615

1716
expect(result).toBe('Bananas')
1817
const bananasBytes = Bytes('Bananas')
19-
const abiLog = ABI_RETURN_VALUE_LOG_PREFIX.concat(bananasBytes)
2018
const logs = ctx.exportLogs(ctx.txn.lastActive.appId.id, 's', 'b')
21-
expect(logs).toStrictEqual([result, abiLog])
19+
expect(logs).toStrictEqual([result, bananasBytes])
2220
})
2321
it('logs the returned value when sayHello is called', async () => {
2422
const contract = ctx.contract.create(HelloWorldContract)
@@ -27,8 +25,7 @@ describe('HelloWorldContract', () => {
2725

2826
expect(result).toBe('Hello John Doe')
2927
const helloBytes = Bytes('Hello John Doe')
30-
const abiLog = ABI_RETURN_VALUE_LOG_PREFIX.concat(helloBytes)
3128
const logs = ctx.exportLogs(ctx.txn.lastActive.appId.id, 's', 'b')
32-
expect(logs).toStrictEqual([result, abiLog])
29+
expect(logs).toStrictEqual([result, helloBytes])
3330
})
3431
})

examples/htlc-logicsig/signature.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { Account, Bytes } from '@algorandfoundation/algorand-typescript'
22
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
33
import algosdk from 'algosdk'
44
import { afterEach, describe, expect, it } from 'vitest'
5-
import { ZERO_ADDRESS } from '../../src/constants'
65
import HashedTimeLockedLogicSig from './signature.algo'
76

7+
const ZERO_ADDRESS_B32 = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ'
8+
const ZERO_ADDRESS = Bytes.fromBase32(ZERO_ADDRESS_B32)
9+
810
describe('HTLC LogicSig', () => {
911
const ctx = new TestExecutionContext()
1012

examples/precompiled/contract.spec.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { arc4 } from '@algorandfoundation/algorand-typescript'
1+
import { arc4, Bytes } from '@algorandfoundation/algorand-typescript'
22
import { TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
33
import { afterEach, describe, it } from 'vitest'
4-
import { ABI_RETURN_VALUE_LOG_PREFIX, MAX_BYTES_SIZE } from '../../src/constants'
5-
import { asUint64Cls } from '../../src/util'
64
import { HelloFactory } from './contract.algo'
75
import { Hello, HelloTemplate, HelloTemplateCustomPrefix, LargeProgram, TerribleCustodialAccount } from './precompiled-apps.algo'
86

7+
const MAX_BYTES_SIZE = 4096
8+
const ABI_RETURN_VALUE_LOG_PREFIX = Bytes.fromHex('151F7C75')
9+
910
describe('pre compiled app calls', () => {
1011
const ctx = new TestExecutionContext()
1112
afterEach(() => {
@@ -58,7 +59,7 @@ describe('pre compiled app calls', () => {
5859
// Arrange
5960
const largeProgramApp = ctx.any.application({
6061
approvalProgram: ctx.any.bytes(20),
61-
appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(asUint64Cls(MAX_BYTES_SIZE).toBytes().asAlgoTs())],
62+
appLogs: [ABI_RETURN_VALUE_LOG_PREFIX.concat(Bytes(MAX_BYTES_SIZE))],
6263
})
6364
ctx.setCompiledApp(LargeProgram, largeProgramApp.id)
6465

examples/voting/contract.algo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export class VotingRoundApp extends arc4.Contract {
154154
note: note,
155155
fee: Global.minTxnFee,
156156
})
157-
.submit().configAsset
157+
.submit().createdAsset
158158
}
159159

160160
@abimethod({ readonly: true })

examples/voting/contract.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Bytes, Uint64 } from '@algorandfoundation/algorand-typescript'
2+
import { bytesToUint8Array, TestExecutionContext } from '@algorandfoundation/algorand-typescript-testing'
3+
import { DynamicArray, UintN8 } from '@algorandfoundation/algorand-typescript/arc4'
4+
import nacl from 'tweetnacl'
5+
import { afterEach, describe, expect, it } from 'vitest'
6+
import { VotingRoundApp } from './contract.algo'
7+
8+
describe('VotingRoundApp', () => {
9+
const ctx = new TestExecutionContext()
10+
11+
const boostrapMinBalanceReq = Uint64(287100)
12+
const voteMinBalanceReq = Uint64(21300)
13+
const tallyBoxSize = 208
14+
const keyPair = nacl.sign.keyPair()
15+
const voteId = ctx.any.string()
16+
17+
const createContract = () => {
18+
const contract = ctx.contract.create(VotingRoundApp)
19+
const snapshotPublicKey = Bytes(keyPair.publicKey)
20+
const metadataIpfsCid = ctx.any.string(16)
21+
const startTime = ctx.any.uint64(Date.now() - 10_000, Date.now())
22+
const endTime = ctx.any.uint64(Date.now() + 10_000, Date.now() + 100_000)
23+
const optionCounts = new DynamicArray<UintN8>(
24+
...Array(13)
25+
.fill(0)
26+
.map(() => new UintN8(2)),
27+
)
28+
const quorum = ctx.any.uint64()
29+
const nftImageUrl = ctx.any.string(64)
30+
contract.create(voteId, snapshotPublicKey, metadataIpfsCid, startTime, endTime, optionCounts, quorum, nftImageUrl)
31+
return contract
32+
}
33+
34+
afterEach(() => {
35+
ctx.reset()
36+
})
37+
38+
it('shoulld be able to bootstrap', () => {
39+
const contract = createContract()
40+
const app = ctx.ledger.getApplicationForContract(contract)
41+
contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq }))
42+
43+
expect(contract.isBootstrapped.value).toEqual(true)
44+
expect(contract.tallyBox.value).toEqual(Bytes.fromHex('00'.repeat(tallyBoxSize)))
45+
})
46+
47+
it('should be able to get pre conditions', () => {
48+
const contract = createContract()
49+
const app = ctx.ledger.getApplicationForContract(contract)
50+
contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq }))
51+
52+
const account = ctx.any.account()
53+
const signature = nacl.sign.detached(bytesToUint8Array(account.bytes), keyPair.secretKey)
54+
ctx.txn.createScope([ctx.any.txn.applicationCall({ sender: account })]).execute(() => {
55+
const preconditions = contract.getPreconditions(Bytes(signature))
56+
57+
expect(preconditions.is_allowed_to_vote).toEqual(1)
58+
expect(preconditions.is_voting_open).toEqual(1)
59+
expect(preconditions.has_already_voted).toEqual(0)
60+
expect(preconditions.current_time).toEqual(ctx.txn.activeGroup.latestTimestamp)
61+
})
62+
})
63+
64+
it('should be able to vote', () => {
65+
const contract = createContract()
66+
const app = ctx.ledger.getApplicationForContract(contract)
67+
contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq }))
68+
69+
const account = ctx.any.account()
70+
const signature = nacl.sign.detached(bytesToUint8Array(account.bytes), keyPair.secretKey)
71+
const answerIds = new DynamicArray<UintN8>(
72+
...Array(13)
73+
.fill(0)
74+
.map(() => new UintN8(Math.ceil(Math.random() * 10) % 2)),
75+
)
76+
77+
ctx.txn.createScope([ctx.any.txn.applicationCall({ appId: app, sender: account })]).execute(() => {
78+
contract.vote(ctx.any.txn.payment({ receiver: app.address, amount: voteMinBalanceReq }), Bytes(signature), answerIds)
79+
80+
expect(contract.votesByAccount.get(account).bytes).toEqual(answerIds.bytes)
81+
expect(contract.voterCount.value).toEqual(13)
82+
})
83+
})
84+
85+
it('should be able to close', () => {
86+
const contract = createContract()
87+
const app = ctx.ledger.getApplicationForContract(contract)
88+
contract.bootstrap(ctx.any.txn.payment({ receiver: app.address, amount: boostrapMinBalanceReq }))
89+
90+
contract.close()
91+
92+
expect(contract.closeTime.value).toEqual(ctx.txn.lastGroup.latestTimestamp)
93+
expect(contract.nftAsset.value.name).toEqual(`[VOTE RESULT] ${voteId}`)
94+
expect(contract.nftAsset.value.unitName).toEqual('VOTERSLT')
95+
})
96+
})

src/decode-logs.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
import { bytes, op } from '@algorandfoundation/algorand-typescript'
2+
import { ABI_RETURN_VALUE_LOG_PREFIX } from './constants'
3+
import { asNumber } from './util'
24

35
export type LogDecoding = 'i' | 's' | 'b'
46

57
export type DecodedLog<T extends LogDecoding> = T extends 'i' ? bigint : T extends 's' ? string : Uint8Array
68
export type DecodedLogs<T extends [...LogDecoding[]]> = {
79
[Index in keyof T]: DecodedLog<T[Index]>
810
} & { length: T['length'] }
11+
12+
const ABI_RETURN_VALUE_LOG_PREFIX_LENGTH = asNumber(ABI_RETURN_VALUE_LOG_PREFIX.length)
913
export function decodeLogs<const T extends [...LogDecoding[]]>(logs: bytes[], decoding: T): DecodedLogs<T> {
1014
return logs.map((log, i) => {
15+
const value = log.slice(0, ABI_RETURN_VALUE_LOG_PREFIX_LENGTH).equals(ABI_RETURN_VALUE_LOG_PREFIX)
16+
? log.slice(ABI_RETURN_VALUE_LOG_PREFIX_LENGTH)
17+
: log
1118
switch (decoding[i]) {
1219
case 'i':
13-
return op.btoi(log)
20+
return op.btoi(value)
1421
case 's':
15-
return log.toString()
22+
return value.toString()
1623
default:
17-
return log
24+
return value
1825
}
1926
}) as DecodedLogs<T>
2027
}

0 commit comments

Comments
 (0)