Voting Contract
Description
Section titled “Description”Example source from examples/voting/contract.algo.ts.
Prerequisites
Section titled “Prerequisites”LocalNet running (algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository’s examples directory:
cd examples
npx tsx voting/contract.algo.ts
import type { Account, Asset, bytes, gtxn, uint64 } from '@algorandfoundation/algorand-typescript'
import {
abimethod,
arc4,
assert,
assertMatch,
Box,
BoxMap,
Bytes,
clone,
ensureBudget,
Global,
GlobalState,
itxn,
log,
op,
OpUpFeeSource,
readonly,
Txn,
Uint64,
urange,
} from '@algorandfoundation/algorand-typescript'
type VoteIndexArray = arc4.DynamicArray<arc4.Uint<8>>
const VOTE_INDEX_BYTES: uint64 = 1
const VOTE_COUNT_BYTES: uint64 = 8
// The min balance increase per box created
const BOX_FLAT_MIN_BALANCE: uint64 = 2500
// The min balance increase per byte of boxes (key included)
const BOX_BYTE_MIN_BALANCE: uint64 = 400
// The min balance increase for each asset opted into
const ASSET_MIN_BALANCE: uint64 = 100000
// TODO: ObjectPType should hopefully respect this ordering of properties
type VotingPreconditions = {
is_voting_open: uint64
is_allowed_to_vote: uint64
has_already_voted: uint64
current_time: uint64
}
export class VotingRoundApp extends arc4.Contract {
isBootstrapped = GlobalState<boolean>({ initialValue: false })
voterCount = GlobalState({ initialValue: Uint64(0) })
closeTime = GlobalState<uint64>()
tallyBox = Box<bytes>({ key: Bytes`V` })
votesByAccount = BoxMap<Account, VoteIndexArray>({ keyPrefix: Bytes() })
voteId = GlobalState<string>()
snapshotPublicKey = GlobalState<bytes<32>>()
metadataIpfsCid = GlobalState<string>()
startTime = GlobalState<uint64>()
nftImageUrl = GlobalState<string>()
endTime = GlobalState<uint64>()
quorum = GlobalState<uint64>()
optionCounts = GlobalState<VoteIndexArray>()
totalOptions = GlobalState<uint64>()
nftAsset = GlobalState<Asset>()
@abimethod({ onCreate: 'require' })
public create(
voteId: string,
snapshotPublicKey: bytes<32>,
metadataIpfsCid: string,
startTime: uint64,
endTime: uint64,
optionCounts: VoteIndexArray,
quorum: uint64,
nftImageUrl: string,
): void {
assert(startTime < endTime, 'End time should be after start time')
assert(endTime >= Global.latestTimestamp, 'End time should be in the future')
this.voteId.value = voteId
this.snapshotPublicKey.value = snapshotPublicKey
this.metadataIpfsCid.value = metadataIpfsCid
this.startTime.value = startTime
this.endTime.value = endTime
this.quorum.value = quorum
this.nftImageUrl.value = nftImageUrl
this.storeOptionCounts(optionCounts)
}
public bootstrap(fundMinBalReq: gtxn.PaymentTxn): void {
assert(!this.isBootstrapped.value, 'Must not be already bootstrapped')
this.isBootstrapped.value = true
assertMatch(
fundMinBalReq,
{
receiver: Global.currentApplicationAddress,
},
'Payment must be to app address',
)
const tallyBoxSize: uint64 = this.totalOptions.value * VOTE_COUNT_BYTES
const minBalanceReq: uint64 =
ASSET_MIN_BALANCE * 2 + 1000 + BOX_FLAT_MIN_BALANCE + BOX_BYTE_MIN_BALANCE + tallyBoxSize * BOX_BYTE_MIN_BALANCE
log(minBalanceReq)
assertMatch(
fundMinBalReq,
{
amount: minBalanceReq,
},
'Payment must be for the exact min balance requirement',
)
assert(this.tallyBox.create({ size: tallyBoxSize }))
}
public close() {
ensureBudget(20000)
assert(!this.closeTime.hasValue, 'Already closed')
this.closeTime.value = Global.latestTimestamp
// Do we need a way to declare string literals where we ignore leading whitespace?
let note = `{
"standard":"arc69",
"description":"This is a voting result NFT for voting round with ID ${this.voteId.value}",
"properties":{
"metadata":"ipfs://${this.metadataIpfsCid.value}",
"id":"${this.voteId.value}",
"quorum":"${itoa(this.quorum.value)}}",
"voterCount":"${itoa(this.voterCount.value)}",
"tallies": [`
let currentIndex = Uint64(0)
for (const [questionIndex, questionOptions] of this.optionCounts.value.entries()) {
if (questionIndex > 0) {
note += ','
}
if (questionOptions.asUint64() > 0) {
note += '['
for (let optionIndex = Uint64(0); optionIndex <= questionOptions.asUint64(); optionIndex++) {
if (optionIndex > 0) {
note += ','
}
note += itoa(this.getVoteFromBox(currentIndex))
currentIndex += 1
}
note += ']'
}
}
note += ']}}'
this.nftAsset.value = itxn
.assetConfig({
total: 1,
decimals: 0,
defaultFrozen: false,
assetName: `[VOTE RESULT] ${this.voteId.value}`,
unitName: `VOTERSLT`,
url: this.nftImageUrl.value,
note: note,
fee: Global.minTxnFee,
})
.submit().createdAsset
}
@readonly
public getPreconditions(signature: bytes<64>): VotingPreconditions {
return {
is_allowed_to_vote: Uint64(this.allowedToVote(signature)),
is_voting_open: Uint64(this.votingOpen()),
has_already_voted: Uint64(this.alreadyVoted()),
current_time: Global.latestTimestamp,
}
}
private allowedToVote(signature: bytes<64>): boolean {
ensureBudget(2000)
return op.ed25519verifyBare(Txn.sender.bytes, signature, this.snapshotPublicKey.value)
}
private alreadyVoted(): boolean {
return this.votesByAccount(Txn.sender).exists
}
public vote(fundMinBalReq: gtxn.PaymentTxn, signature: bytes<64>, answerIds: VoteIndexArray): void {
ensureBudget(7700, OpUpFeeSource.GroupCredit)
assert(this.allowedToVote(signature), 'Not allowed to vote')
assert(this.votingOpen(), 'Voting not open')
assert(!this.alreadyVoted(), 'Already voted')
const questionsCount = this.optionCounts.value.length
assertMatch(
answerIds,
{
length: questionsCount,
},
'Number of answers incorrect',
)
const minBalReq: uint64 = BOX_FLAT_MIN_BALANCE + (32 + 2 + VOTE_INDEX_BYTES * answerIds.length) * BOX_BYTE_MIN_BALANCE
log(minBalReq)
assertMatch(
fundMinBalReq,
{
receiver: Global.currentApplicationAddress,
amount: minBalReq,
},
'Payment must be to app and for exactly min balance',
)
let cumulativeOffset = Uint64(0)
for (const questionIndex of urange(questionsCount)) {
const answerOptionIndex = answerIds.at(questionIndex).asUint64()
const optionsCount = this.optionCounts.value.at(questionIndex).asUint64()
assert(answerOptionIndex < optionsCount, 'Answer option index invalid')
this.incrementVoteInBox(cumulativeOffset + answerOptionIndex)
cumulativeOffset += optionsCount
this.votesByAccount(Txn.sender).value = clone(answerIds)
this.voterCount.value += 1
}
}
private votingOpen(): boolean {
return (
this.isBootstrapped.value &&
!this.closeTime.hasValue &&
this.startTime.value <= Global.latestTimestamp &&
Global.latestTimestamp <= this.endTime.value
)
}
private storeOptionCounts(optionCounts: VoteIndexArray) {
assertMatch(optionCounts, { length: { between: [Uint64(1), Uint64(112)] } })
let totalOptions = Uint64(0)
for (const item of optionCounts) {
totalOptions += item.asUint64()
}
this.optionCounts.value = clone(optionCounts)
this.totalOptions.value = totalOptions
}
private getVoteFromBox(index: uint64): uint64 {
return op.btoi(this.tallyBox.extract(index, VOTE_COUNT_BYTES))
}
private incrementVoteInBox(index: uint64): void {
const currentVote = this.getVoteFromBox(index)
this.tallyBox.replace(index, op.itob(currentVote + 1))
}
}
function itoa(i: uint64): string {
const digits = Bytes`0123456789`
const radix = digits.length
if (i < radix) {
return digits.at(i).toString()
}
return `${itoa(i / radix)}${digits.at(i % radix)}`
}
Other examples
Section titled “Other examples”- ARC4 Simple Voting Contract
- Auction
- Calculator Contract
- Hello World Contract
- Hello World ABI Contract
- Htlc Logicsig Signature
- Local Storage Contract
- Marketplace Contract
- Precompiled Precompiled Apps
- Precompiled Precompiled Factory
- Precompiled Precompiled Typed
- Proof Of Attendance Contract
- Scratch Storage Contract
- Simple Voting
- Tealscript Example
- Tealscript Teal Script Base
- Voting Contract
- ZK Whitelist