Poll Contract
The poll contract stores polls' IPFS hash and users' answers. It guarantees that an account can only answer once to a poll. It also computes the number of times an answer has been selected.
When a user adds a poll (IPFS hash), it needs to be approved by a special account, called contract's owner. The owner can also remove any existing poll.
Repository
Author
Norms
Project
Poll contract project was created with the following Completium CLI command:
completium-cli create project poll-contract
Command npm i installs required packages:
- typescript util packages
mochafor test suite- archetype's packages for binding
package.json file is created with utility commands, including:
npm run "gen-binding"to generate contract(s)' bindingnpm testto launch tests intestsdirectory
Deployment
The following Completium CLI command is used to deploy the contract:
completium-cli deploy ./contracts/poll.arl --parameters '{ "owner": "tz1h4CiqWxNe4UxSpkwXy617RM6DaK6NU76P" }' --metadata-uri "ipfs://QmXbuUyyJXW1RRuL3k81Kpe2HULbYLj1sUUq44Nuxa5z8h"
where QmXbuUyyJXW1RRuL3k81Kpe2HULbYLj1sUUq44Nuxa5z8h is the IPFS hash of the contract metadata file:
{
"name": "Poll Dapp",
"description": "An example of Dapp built with Archetype & Completium",
"version": "2.0",
"license": { "name": "MIT" },
"authors": ["Completium team <contact@completium.com>"],
"homepage": "https://completium.github.io/poll-dapp/",
"interfaces": ["TZIP-016"]
}
Storage
owner
Contract owner's address, passed as contract parameter. Only the owner can:
- approve a poll
- remove a poll
- transfer contract ownership
- pause contract
- unpause contract
Code
- Archetype
- TS Binding API
archetype poll(owner : address)
with metadata ""
(method) Poll.get_owner() : Promise<Address>
poll_counter
Number of polls added, used as poll key in poll asset collection (see approve entry point).
Code
- Archetype
- TS Binding API
poll
Collection of polls.
A poll is identified by a natural integer rather than by its IPFS hash. This is to minimize the required storage of the responder information, that stores which polls an user has answered.
The responses field stores the numbers of responses to poll's possible answers.
Code
- Archetype
- TS Binding API
asset poll {
poll_pk : nat;
ipfs_hash : bytes;
creation : date = now;
responses : map<nat, nat> = []
}
export type poll_key = Nat
export class poll_value {
constructor(
ipfs_hash: Bytes,
responses: Array<[ Nat, Nat ]>,
creation: Date)
}
export type poll_container = Array<[ poll_key, poll_value ]>;
(method) Poll.get_poll(): Promise<poll_container>
poll_to_approve
Collection of polls' IPFS hashes proposed by users. When approved by owner, a poll asset is created.
Note that the asset collection is created as a big_map, to be able to handle an arbitrary large amount of poll proposition.
Code
- Archetype
- TS Binding API
asset poll_to_approve to big_map {
ipfs_hash_to_approve : bytes;
poll_creator : address = caller
}
export type poll_to_approve_value = Address;
export type poll_to_approve_key = Bytes;
(method) Poll.get_poll_to_approve_value(key: poll_to_approve_key): Promise<poll_to_approve_value | undefined>
(method) Poll.has_poll_to_approve_value(key: poll_to_approve_key): Promise<boolean>
responder
Collection of responders' lists (set) of answered polls. This is to decide whether a responder has already answered a poll or not (see respond entrypoint).
It is specified as a big_map to be able to handle an arbitrary large number of responders.
Code
- Archetype
- TS Binding API
export type responder_key = Address
export type responder_value = Array<Nat>
(method) Poll.get_responder_value(key: responder_key): Promise<responder_value | undefined>
(method) Poll.has_responder_value(key: responder_key): Promise<boolean>
Entry
add_poll
Entry to call to propose a new poll. The poll's IPFS hash is added to the collection of hashes to approve.
Code
- Archetype
- TS Binding API
- Ts Test
entry add_poll(h : bytes) {
require {
r1 : is_not_paused()
}
effect {
poll_to_approve.add({ ipfs_hash_to_approve = h });
emit<NewPoll>({ caller; h })
}
}
(method) Poll.add_poll(h: Bytes, params: Partial<Parameters>): Promise<any>
describe("[POLL] 'add_poll' entry", async () => {
it("add 'Food' poll", async () => {
const b = Bytes.hex_encode(food_hash)
await poll.add_poll(Bytes.hex_encode(food_hash), { as: bob });
const has_poll = await poll.has_poll_to_approve_value(b)
assert(has_poll)
})
it("'add' cannot be called with same hash", async () => {
const b = Bytes.hex_encode(food_hash)
expect_to_fail(async () => {
await poll.add_poll(Bytes.hex_encode(food_hash), { as: alice });
}, error_key_exists("poll_to_approve"))
})
it("add 'Dancer' poll", async () => {
const b = Bytes.hex_encode(dancer_hash)
await poll.add_poll(b, { as: bob });
const has_poll = await poll.has_poll_to_approve_value(b)
assert(has_poll)
})
it("add 'Squares' poll", async () => {
const b = Bytes.hex_encode(squares_hash)
await poll.add_poll(b, { as: bob });
const has_poll = await poll.has_poll_to_approve_value(b)
assert(has_poll)
})
})
Parameter
Fails with
CONTRACT_PAUSED
owner("KEY_EXISTS", "poll_to_approve")
h has already been proposedEmits
Related
respond
Entry to call to answer a poll. It fails if:
- the poll hash is not registered
- the caller has already responded
The number of times someone has responded to the poll's answer (choice_id) is incremented, and the poll id is registered in the set of polls caller has already responded to.
Code
- Archetype
- TS Binding API
- TS Test
entry respond(pk : nat, choice_id : nat) {
constant {
selection_count is poll[pk] ? (the.responses[choice_id] ? the : 0) : 0;
}
require {
r2 : is_not_paused();
r3 : poll.contains(pk) otherwise POLL_NOT_FOUND;
}
fail if {
f1 : responder[caller] ? the.polls.contains(pk) : false with CANNOT_RESPOND_TWICE
}
effect {
responder.add_update(caller, { polls += [pk] } );
poll.update(pk, {
responses += [(choice_id, selection_count + 1)]
});
emit<Response>({ caller; pk; choice_id })
}
}
entry nat constant require is_not_paused contains fail if effect add_update += update emit caller
(method) Poll.respond(pk: Nat, choice_id: Nat, params: Partial<Parameters>): Promise<any>
describe("[POLL] 'respond' entry", async () => {
it("respond to food poll", async () => {
const poll_id = new Nat(0)
const choice_id = new Nat(0)
const polls = await poll.get_poll()
const nb_responses_before = get_nb_responses(polls, poll_id, choice_id)
assert(nb_responses_before.equals(new Nat(0)))
const has_responder_before = await poll.has_responder_value(bob.get_address())
assert(!has_responder_before)
await poll.respond(poll_id, choice_id, { as : bob })
const polls_after = await poll.get_poll()
const nb_responses_after = get_nb_responses(polls_after, poll_id, choice_id)
assert(nb_responses_after.equals(new Nat(1)))
const has_responder_after = await poll.has_responder_value(bob.get_address())
assert(has_responder_after)
})
it("responder cannot respond twice", async () => {
const poll_id = new Nat(0)
const choice_id = new Nat(1)
expect_to_fail(async () => {
await poll.respond(poll_id, choice_id, { as : bob })
}, poll.errors.f1)
})
it("'respond' increment number of responses", async () => {
const poll_id = new Nat(0)
const choice_id = new Nat(0)
const polls = await poll.get_poll()
const nb_responses_before = get_nb_responses(polls, poll_id, choice_id)
await poll.respond(poll_id, choice_id, { as : carl })
const polls_after = await poll.get_poll()
const nb_responses_after = get_nb_responses(polls_after, poll_id, choice_id)
assert(nb_responses_after.equals(nb_responses_before.plus(new Nat(1))))
})
})
Parameters
Called by owner
approve
Entry called by owner to approve a proposed poll:
- a new poll is added to the
pollasset collection - the proposed IPFS hash is removed from
poll_to_approve
Code
- Archetype
- TS Binding API
- TS Test
entry approve(h : bytes) {
called by owner
constant {
creator_ ?is poll_to_approve[h]?.poll_creator otherwise POLL_NOT_FOUND
}
effect {
poll.add({ poll_pk = polls_counter; ipfs_hash = h });
polls_counter += 1;
poll_to_approve.remove(h);
emit<Approve>({ creator_; h })
}
}
entry bytes called by constant ?is ?. effect add += remove emit
(method) Poll.approve(h: Bytes, params: Partial<Parameters>): Promise<any>
describe("[POLL] 'approve' entry", async () => {
it("'approve' can only be called by owner", async () => {
expect_to_fail(async () => {
await poll.approve(Bytes.hex_encode(food_hash), { as: bob });
}, poll.errors.INVALID_CALLER)
})
it("approve 'Food' poll", async () => {
const b = Bytes.hex_encode(food_hash)
const polls_before = await poll.get_poll()
assert(!exists_poll(polls_before, b))
await poll.approve(b, { as: alice });
const polls_after = await poll.get_poll()
assert(exists_poll(polls_after, b))
const has_poll = await poll.has_poll_to_approve_value(b)
assert(!has_poll)
})
it("'approve' cannot be called twice with same hash", async () => {
const b = Bytes.hex_encode(food_hash)
expect_to_fail(async () => {
await poll.approve(b, { as: alice });
}, poll.errors.POLL_NOT_FOUND)
})
it("approve 'Dancer' poll", async () => {
const b = Bytes.hex_encode(dancer_hash)
const polls_before = await poll.get_poll()
assert(!exists_poll(polls_before, b))
await poll.approve(b, { as: alice });
const polls_after = await poll.get_poll()
assert(exists_poll(polls_after, b))
const has_poll = await poll.has_poll_to_approve_value(b)
assert(!has_poll)
})
})
Parameter
Fails with
POLL_NOT_FOUND
h is not found in poll_to_approve assetEmits
Related
disapprove
Entry called by owner to disapprove a proposed poll:
- the proposed IPFS hash is removed from
poll_to_approve
Code
- Archetype
- TS Binding API
- TS Test
entry disapprove(h : bytes) {
called by owner
effect {
poll_to_approve.remove(h)
}
}
(method) Poll.disapprove(h: Bytes, params: Partial<Parameters>): Promise<any>
describe("[POLL] 'disapprove' entry", async () => {
it("'disapprove' can only be called by owner", async () => {
expect_to_fail(async () => {
await poll.disapprove(Bytes.hex_encode(squares_hash), { as: bob });
}, poll.errors.INVALID_CALLER)
})
it("disapprove 'Squares' poll", async () => {
const b = Bytes.hex_encode(squares_hash)
const polls_before = await poll.get_poll()
assert(!exists_poll(polls_before, b))
await poll.disapprove(b, { as: alice });
const polls_after = await poll.get_poll()
assert(!exists_poll(polls_after, b))
const has_poll = await poll.has_poll_to_approve_value(b)
assert(!has_poll)
})
})
remove
Entry called by owner to remove a poll.
Code
- Archetype
- TS Binding API
- TS Test
entry remove(pk : nat) {
called by owner
effect {
poll.remove(pk)
}
}
(method) Poll.remove(pk: Nat, params: Partial<Parameters>): Promise<any>
describe("[POLL] 'remove' entry", async () => {
it("'remove' can only be called by owner", async () => {
expect_to_fail(async () => {
await poll.remove(new Nat(0), { as : bob })
}, poll.errors.INVALID_CALLER)
})
it("remove food poll", async () => {
const polls_before = await poll.get_poll()
assert(exists_poll(polls_before, Bytes.hex_encode(food_hash)))
await poll.remove(new Nat(0), { as : alice })
const polls = await poll.get_poll()
assert(!exists_poll(polls, Bytes.hex_encode(food_hash)))
})
})
View
get_responses
Returns poll pk response statistics.
Code
- Archetype
- TS Binding API
view get_responses(pk : nat) : map<nat, nat> {
return poll[pk].responses
}
(method) Poll.view_get_responses(pk: Nat, params: Partial<Parameters>): Promise<Array<[ Nat, Nat ]>>
already_responded
Returns true if source has already answered poll pk.
Code
- Archetype
- TS Binding API
view already_responded(pk : nat) : bool {
return (responder[source] ? the.polls.contains(pk) : false)
}
(method) Poll.view_already_responded(pk: Nat, params: Partial<Parameters>): Promise<boolean>
Events
NewPoll
Emitted by add_poll with:
- poll creator's address
- poll's IPFS hash
Code
- Archetype
- TS Binding API
Response
Emitted by respond with:
- responder's address
- poll's id
- response's id
Code
- Archetype
- TS Binding API
Approval
Emitted by approve with:
- proposal issuer's address
- poll's IPFS hash
Code