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
mocha
for 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 test
to launch tests intests
directory
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
poll
asset 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