Add vote management
In the previous tutorial, you built and deployed an election smart contract on the local development network.
You then connected the frontend built with the @kadena/client
library to the development network backend.
After connecting the frontend to the development network backend, you were able to add a candidate to the candidates
database table in the Pact election
module and see the results in the election application website.
In this tutorial, you'll update the election
module to allow anyone with a Kadena account to cast a vote on a candidate.
After you update the backend functionality, you'll modify the frontend to use the development network so that Kadena account holders can vote using the election application website and have their votes recorded on the blockchain, ensuring the security and transparency of the election process.
Before you begin
Before you start this tutorial, verify the following basic requirements:
- You have an internet connection and a web browser installed on your local computer.
- You have a code editor, such as Visual Studio Code, access to an interactive terminal shell, and are generally familiar with using command-line programs.
- You have cloned the voting-dapp repository to create your project directory as described in Prepare your workspace.
- You have the development network running in a Docker container as described in Start a local blockchain.
- You are connected to the development network using your local host IP address and port number 8080.
- You have created and funded an administrative account as described in Add an administrator account.
- You have created a principal namespace on the development network as described in Define a namespace.
- You have defined the keyset that controls your namespace using the administrative account as described in Define keysets.
- You have created an election Pact module and deployed it as described in Write a smart contract and updated its functionality as described in Nominate candidates.
Increment votes for a candidate
When an account holder clicks Vote Now in the election application, it triggers a call to the vote
function in the frontend/src/repositories/vote/DevnetVoteRepository.ts
file, passing the account name and the name of the candidate corresponding to the table row that was clicked.
The vote
function in the frontend uses the Kadena client to execute the vote
function defined in the election
module.
To implement the vote
function in the election
Pact module, you can test your code as you go using the Pact REPL as you did in previous tutorials.
Organize your REPL files
So far, you have added all of your tests for the election
module to the election-dapp/pact/election.repl
file.
While this is convenient if you have a small number of tests, continuing to add tests to a single file will make testing more complex and more difficult to follow.
To keep tests more organized, you can split them into multiple .repl
files and reuse the code by loading one file into the other.
To organize tests into separate files:
-
Open the
election-dapp/pact
folder in the code editor on your computer. -
Rename
election.repl
tocandidates.repl
. -
Create a new
setup.repl
file in thepact
folder. -
Move the code before
(begin-tx "Load election module")
from thecandidates.repl
into thesetup.repl
file. -
Create a new
voting.repl
file in thepact
folder and add the following as the first line in the file:(load "setup.repl")
(load "setup.repl")
-
Open the
candidates.repl
file and and add the following as the first line in the file:(load "setup.repl")
(load "setup.repl")
-
Verify tests in the
candidates.repl
file still pass by running the following command:pact candidates.repl --trace
pact candidates.repl --trace
-
Verify that
voting.repl
loads successfully by running the following command:pact voting.repl --trace
pact voting.repl --trace
Prepare a test for incrementing votes
Based on the work you did in the previous tutorial, the election application website displays a table of the candidates you have added.
Each candidate starts with zero (0) votes.
Each row in the table has a Vote Now button.
If you click Vote Now, the number of votes displayed in corresponding row should be increased by one.
The table is rendered based on the result of a call to the list-candidates
function of the election
Pact module.
So, in the Pact REPL you can test the behavior of the new vote
function against the return value of list-candidates
.
To prepare a test for incrementing votes:
-
Open the
election-dapp/pact/voting.repl
file in the code editor on your computer. -
Add transactions to load the
election
Pact module and to add a candidate to thecandidates
table:(begin-tx "Load election module") (load "election.pact")(commit-tx) (begin-tx "Add a candidate") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (add-candidate { "key": "1", "name": "Candidate A" })(commit-tx)
(begin-tx "Load election module") (load "election.pact")(commit-tx) (begin-tx "Add a candidate") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (add-candidate { "key": "1", "name": "Candidate A" })(commit-tx)
Remember to replace the namespace with your own principal namespace.
-
Add the following lines of code to test a voting transaction:
(begin-tx "Voting for a candidate") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect "Candidate A has 0 votes" 0 (at 'votes (at 0 (list-candidates))) ) (vote "1") (expect "Candidate A has 1 vote" 1 (at 'votes (at 0 (list-candidates))) )(commit-tx)
(begin-tx "Voting for a candidate") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect "Candidate A has 0 votes" 0 (at 'votes (at 0 (list-candidates))) ) (vote "1") (expect "Candidate A has 1 vote" 1 (at 'votes (at 0 (list-candidates))) )(commit-tx)
This code:
- Verifies that the candidate is initialized with zero votes.
- Calls the
vote
function with the key value (1
) of the candidate as the only argument. - Asserts that the candidate has one vote.
If you were to execute the transaction, the test would fail because the
vote
function doesn't exist yet in theelection
module and you would see output similar to the following:voting.repl:18:5:Error: Cannot resolve voteLoad failed
voting.repl:18:5:Error: Cannot resolve voteLoad failed
-
Open the
election-dapp/pact/election.pact
file in your code editor. -
Define the
vote
function after theadd-candidate
function with the following lines of code:(defun vote (candidateKey:string) (with-read candidates candidateKey { "name" := name, "votes" := numberOfVotes } (update candidates candidateKey { "votes": (+ numberOfVotes 1) }) ))
(defun vote (candidateKey:string) (with-read candidates candidateKey { "name" := name, "votes" := numberOfVotes } (update candidates candidateKey { "votes": (+ numberOfVotes 1) }) ))
In this code, the
vote
function takes thecandidateKey
parameter with a type of string:- The
candidateKey
value specifies the key for the row in thecandidates
table to read using the built-inwith-read
Pact function. - The database column named
"votes"
is assigned a value from thenumberOfVotes
variable.
The
vote
function then calls the built-inupdate
Pact function with three arguments to specify:- The table to update (
candidates
). - The key for the row to update (
candidateKey
). - An object with the column names to update and the new value for the respective columns.
In this case, the
vote
function only updates thevotes
column. The new value is the current number of votes that was obtained fromwith-read
and stored in thenumberOfVotes
variable incremented by one ((+ numberOfVotes 1)
).
- The
-
Execute the transaction using the
pact
command-line program:pact voting.repl --trace
pact voting.repl --trace
You should see the transaction succeeds with output similar to the following:
voting.repl:13:5:Trace: Expect: success: Candidate A has 0 votesvoting.repl:18:5:Trace: Write succeededvoting.repl:19:5:Trace: Expect: success: Candidate A has 1 votevoting.repl:24:3:Trace: Commit Tx 4: Voting for a candidateLoad successful
voting.repl:13:5:Trace: Expect: success: Candidate A has 0 votesvoting.repl:18:5:Trace: Write succeededvoting.repl:19:5:Trace: Expect: success: Candidate A has 1 votevoting.repl:24:3:Trace: Commit Tx 4: Voting for a candidateLoad successful
Prepare a test for voting on an invalid candidate
To make the vote
function more robust, you should handle the scenario where the candidateKey
doesn't exist in the database.
To prepare a test for votes on an invalid candidate:
-
Open the
election-dapp/pact/voting.repl
file in the code editor on your computer. -
Add the following transaction before the
Voting for a candidate
transaction:(begin-tx "Voting for a non-existing candidate") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Cannot vote for a non-existing candidate" (vote "20") )(commit-tx)
(begin-tx "Voting for a non-existing candidate") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Cannot vote for a non-existing candidate" (vote "20") )(commit-tx)
Remember to replace the namespace with your own principal namespace.
-
Execute the transaction using the
pact
command-line program:pact voting.repl --trace
pact voting.repl --trace
You should see that the transaction succeeds with output similar to the following:
voting.repl:11:0:Trace: Begin Tx 4: Voting for a non-existing candidatevoting.repl:12:5:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:13:5:Trace: Expect failure: success: Cannot vote for a non-existing candidatevoting.repl:17:0:Trace: Commit Tx 4: Voting for a non-existing candidate
voting.repl:11:0:Trace: Begin Tx 4: Voting for a non-existing candidatevoting.repl:12:5:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:13:5:Trace: Expect failure: success: Cannot vote for a non-existing candidatevoting.repl:17:0:Trace: Commit Tx 4: Voting for a non-existing candidate
The test returns the expected result—failure—because the call to
with-read
fails for thecandidateKey
value of"20"
. The failure prevents the execution of theupdate
function.As you add checks to the
vote
function, you should return more specific error messages, so that each check provides information about why it failed to the caller of the function. -
Update the invalid candidate transaction to specify
"Candidate does not exist"
as the expected error message:(begin-tx "Voting for a non-existing candidate")(use n_14912521e87a6d387157d526b281bde8422371d1.election)(expect-failure "Cannot vote for a non-existing candidate" "Candidate does not exist" (vote "X"))(commit-tx)
(begin-tx "Voting for a non-existing candidate")(use n_14912521e87a6d387157d526b281bde8422371d1.election)(expect-failure "Cannot vote for a non-existing candidate" "Candidate does not exist" (vote "X"))(commit-tx)
In this code:
- This first argument of
expect-failure
is the name of the test. - The second argument is the expected output of the function call.
- The third argument is the actual function call.
- This first argument of
-
Execute the transaction using the
pact
command-line program:pact voting.repl --trace
pact voting.repl --trace
You should see that the transaction fails with output similar to the following:
voting.repl:11:0:Trace: Begin Tx 4: Voting for a non-existing candidatevoting.repl:12:5:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:13:5:Trace: FAILURE: Cannot vote for a non-existing candidate: expected error message to contain 'Candidate does not exist', got '(with-read candidates candidat...: Failure: Tx Failed: with-read: row not found: X'voting.repl:18:0:Trace: Commit Tx 4: Voting for a non-existing candidate
voting.repl:11:0:Trace: Begin Tx 4: Voting for a non-existing candidatevoting.repl:12:5:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:13:5:Trace: FAILURE: Cannot vote for a non-existing candidate: expected error message to contain 'Candidate does not exist', got '(with-read candidates candidat...: Failure: Tx Failed: with-read: row not found: X'voting.repl:18:0:Trace: Commit Tx 4: Voting for a non-existing candidate
Because the error message doesn't contain the expected output of
Candidate does not exist
that you specified in the previous step, thewith-read
function returns a default error message. If you want to provide a more specific error message, you can use the built-inwith-default-read
Pact function. Thewith-default-read
function enables you to return a default object with default values or a specific error message if a specific condition is detected. In this example, you can use thewith-default-read
function to set the default values for the name (""
) and votes (0
) columns. -
Open the
election-dapp/pact/election.pact
file in your code editor. -
Update the
vote
function to use thewith-default-read
function:(defun vote (candidateKey:string) (with-default-read candidates candidateKey { "name": "", "votes": 0 } { "name" := name, "votes" := numberOfVotes } (enforce (> (length name) 0) "Candidate does not exist") (update candidates candidateKey { "votes": (+ numberOfVotes 1) }) ))
(defun vote (candidateKey:string) (with-default-read candidates candidateKey { "name": "", "votes": 0 } { "name" := name, "votes" := numberOfVotes } (enforce (> (length name) 0) "Candidate does not exist") (update candidates candidateKey { "votes": (+ numberOfVotes 1) }) ))
With this code, a successful read operation assigns the value of the
"name"
column to aname
variable and the value of the"votes"
column to thenumberOfVotes
variable. The function also checks that the candidatename
associated with thecandidateKey
is not an empty string, and returns a specific error if it is. -
Execute the transaction using the
pact
command-line program:pact voting.repl --trace
pact voting.repl --trace
You should see that the transaction succeeds with output similar to the following:
voting.repl:12:0:Trace: Begin Tx 4: Voting for a non-existing candidatevoting.repl:13:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:14:2:Trace: Expect failure: success: Cannot vote for a non-existing candidatevoting.repl:19:0:Trace: Commit Tx 4: Voting for a non-existing candidate...Load successful
voting.repl:12:0:Trace: Begin Tx 4: Voting for a non-existing candidatevoting.repl:13:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:14:2:Trace: Expect failure: success: Cannot vote for a non-existing candidatevoting.repl:19:0:Trace: Commit Tx 4: Voting for a non-existing candidate...Load successful
Prevent double votes
At this point, the election
smart contract allows voting, but it doesn't yet restrict each Kadena account to only voting once.
To keep track of the accounts that have already voted, you can create a new votes
table that uses the account name for each voter as the key and the candidate key as the only column.
In addition to a check against this table, you'll also need to check the keyset used to sign each voting transaction.
Define votes schema and table
To define the database schema and table:
-
Open the
election-dapp/pact/election.pact
file in your code editor. -
Add the schema for the
votes
database table inside of theelection
module definition after the definition of thecandidates
schema and table with the following lines of code:(defschema votes-schema candidateKey:string ) (deftable votes:{votes-schema})
(defschema votes-schema candidateKey:string ) (deftable votes:{votes-schema})
-
Create the table outside of the election module by adding the following lines of code at the end of
./pact/election.pact
, after theelection
module definition and theinit-candidates
code snippet:(if (read-msg "init-votes") [(create-table votes)] [])
(if (read-msg "init-votes") [(create-table votes)] [])
With this code,
read-msg
reads theinit-votes
field from the transaction data. If you set this field totrue
in your module deployment transaction, the statement between the first square brackets is executed. This statement creates thevotes
table based on its schema definition inside the module when you load the module into the Pact REPL or upgrade the module on the blockchain. -
Open the
election-dapp/pact/setup.repl
file in your code editor. -
Add
, 'init-votes: true
to theenv-data
so that this data is loaded in the Pact REPL environment when you load theelection
module and thevotes
table is created:(env-data { 'admin-keyset: { 'keys : [ "5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" ] , 'pred : 'keys-all } , 'init-candidates: true , 'init-votes: true })
(env-data { 'admin-keyset: { 'keys : [ "5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" ] , 'pred : 'keys-all } , 'init-candidates: true , 'init-votes: true })
-
Execute the transaction using the
pact
command-line program:pact voting.repl --trace
pact voting.repl --trace
You should see that the transaction succeeds with
TableCreated
twice in the output similar to the following:election.pact:48:0:Trace: ["TableCreated"]election.pact:53:0:Trace: ["TableCreated"]
election.pact:48:0:Trace: ["TableCreated"]election.pact:53:0:Trace: ["TableCreated"]
Test the votes table
To test that an account can only vote once:
-
Open the
election-dapp/pact/voting.repl
file in the code editor on your computer. -
Add the following transaction to assert that it is not possible to cast more than one vote:
(begin-tx "Double vote") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Cannot vote more than once" "Multiple voting not allowed" (vote "1") )(commit-tx)
(begin-tx "Double vote") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Cannot vote more than once" "Multiple voting not allowed" (vote "1") )(commit-tx)
Remember to replace the namespace with your own principal namespace.
-
Execute the transaction using the
pact
command-line program:pact voting.repl --trace
pact voting.repl --trace
You should see that the transaction fails with output similar to the following:
voting.repl:37:5:Trace: FAILURE: Cannot vote more than once: expected failure, got result = "Write succeeded"voting.repl:42:3:Trace: Commit Tx 6: Double votevoting.repl:37:5:ExecError: FAILURE: Cannot vote more than once: expected failure, got result = "Write succeeded"Load failed
voting.repl:37:5:Trace: FAILURE: Cannot vote more than once: expected failure, got result = "Write succeeded"voting.repl:42:3:Trace: Commit Tx 6: Double votevoting.repl:37:5:ExecError: FAILURE: Cannot vote more than once: expected failure, got result = "Write succeeded"Load failed
Remember that all transactions in
voting.repl
are signed with theadmin-keyset
you defined for the REPL environment in thesetup.repl
file. Your administrative account can cast more than one vote onCandidate A
, which makes the election unfair.To fix this issue, you'll need to update the
vote
function in theelection
module. -
Open the
election-dapp/pact/election.pact
file in your code editor. -
Update the
vote
function to include the account name and prevent the same account from voting more than once:(defun vote (account:string candidateKey:string) (let ((double-vote (account-voted account))) (enforce (= double-vote false) "Multiple voting not allowed")) (with-default-read candidates candidateKey { "name": "", "votes": 0 } { "name" := name, "votes" := numberOfVotes } (enforce (> (length name) 0) "Candidate does not exist") (update candidates candidateKey { "votes": (+ numberOfVotes 1) }) (insert votes account { "candidateKey": candidateKey }) ))
(defun vote (account:string candidateKey:string) (let ((double-vote (account-voted account))) (enforce (= double-vote false) "Multiple voting not allowed")) (with-default-read candidates candidateKey { "name": "", "votes": 0 } { "name" := name, "votes" := numberOfVotes } (enforce (> (length name) 0) "Candidate does not exist") (update candidates candidateKey { "votes": (+ numberOfVotes 1) }) (insert votes account { "candidateKey": candidateKey }) ))
This code:
- Adds the account of the voter as the first parameter of the
vote
function. - Stores the result from a new
account-voted
function in thedouble-vote
variable and uses that value to prevent an account from voting more than once. - Enforces that no row in the
votes
table is keyed with the account name using thewith-default-read
pattern that you used to prevent voting on a non-existent candidate. - Inserts a new row into the
votes
table with the account name as the key and the candidate key as the value for thecandidateKey
column every time thevote
function is called.
- Adds the account of the voter as the first parameter of the
-
Add the
account-voted
function to check if an account has already voted:(defun account-voted:bool (account:string) (with-default-read votes account { "candidateKey": "" } { "candidateKey" := candidateKey } (> (length candidateKey) 0) ))
(defun account-voted:bool (account:string) (with-default-read votes account { "candidateKey": "" } { "candidateKey" := candidateKey } (> (length candidateKey) 0) ))
The frontend of the election application can then use the result from the
account-voted
function to determine if Vote Now should be enabled. -
Open the
election-dapp/pact/voting.repl
file in the code editor on your computer. -
Update all calls to the
vote
function to pass your administrative account name as the first argument.For example, update the
vote
function in theDouble vote
transaction:(begin-tx "Double vote") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Cannot vote more than once" "Multiple voting not allowed" (vote "k:5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" "1") )(commit-tx)
(begin-tx "Double vote") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Cannot vote more than once" "Multiple voting not allowed" (vote "k:5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" "1") )(commit-tx)
-
Execute the transaction using the
pact
command-line program:pact voting.repl --trace
pact voting.repl --trace
You should see that the transaction succeeds with output similar to the following:
voting.repl:35:3:Trace: Begin Tx 6: Double votevoting.repl:36:5:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:37:5:Trace: Expect failure: success: Cannot vote more than oncevoting.repl:42:3:Trace: Commit Tx 6: Double voteLoad successful
voting.repl:35:3:Trace: Begin Tx 6: Double votevoting.repl:36:5:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:37:5:Trace: Expect failure: success: Cannot vote more than oncevoting.repl:42:3:Trace: Commit Tx 6: Double voteLoad successful
With these changes, the same account can't call the
vote
function more than once.
Prevent voting on behalf of other accounts
The current implementation of the vote
function does, however, allow the administrative
account to vote on behalf of other accounts.
To demonstrate voting on behalf of another account:
-
Open the
election-dapp/pact/setup.repl
file in the code editor on your computer. -
Add a
voter-keyset
toenv-data
so that this data is loaded in the Pact REPL environment when you load theelection
module:, 'voter-keyset: { "keys": ["voter"], "pred": "keys-all" }
, 'voter-keyset: { "keys": ["voter"], "pred": "keys-all" }
-
Load the
coin
module and the interfaces it implements with the following lines of code in thesetup.repl
:(begin-tx "Set up coin") (load "root/fungible-v2.pact") (load "root/fungible-xchain-v1.pact") (load "root/coin-v5.pact") (create-table coin.coin-table) (create-table coin.allocation-table) (coin.create-account "voter" (read-keyset "voter-keyset")) (coin.create-account "k:5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" (read-keyset "admin-keyset"))(commit-tx)
(begin-tx "Set up coin") (load "root/fungible-v2.pact") (load "root/fungible-xchain-v1.pact") (load "root/coin-v5.pact") (create-table coin.coin-table) (create-table coin.allocation-table) (coin.create-account "voter" (read-keyset "voter-keyset")) (coin.create-account "k:5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" (read-keyset "admin-keyset"))(commit-tx)
This code:
- Creates the
coin.coin-table
andcoin.allocation-table
required to create thevoter
account. - Creates the
voter
account and your administrative account in thecoin
module database.
Remember to replace the administrative account name with your own account name.
- Creates the
-
Open the
election-dapp/pact/voting.repl
file in the code editor on your computer. -
Add a transaction at the end of the file to cast a vote on behalf of the
voter
account signed by theadmin-keyset
.(begin-tx "Vote on behalf of another account") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Voting on behalf of another account should not be allowed" "Keyset failure (keys-all): [voter]" (vote "voter" "1") )(commit-tx)
(begin-tx "Vote on behalf of another account") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Voting on behalf of another account should not be allowed" "Keyset failure (keys-all): [voter]" (vote "voter" "1") )(commit-tx)
Remember to replace the namespace with your own principal namespace.
-
Execute the transaction using the
pact
command-line program:pact voting.repl --trace
pact voting.repl --trace
You should see that the transaction fails with output similar to the following:
voting.repl:51:0:Trace: Commit Tx 8: Vote on behalf of another accountvoting.repl:46:2:ExecError: FAILURE: Voting on behalf of another account should not be allowed: expected failure, got result = "Write succeeded"Load failed
voting.repl:51:0:Trace: Commit Tx 8: Vote on behalf of another accountvoting.repl:46:2:ExecError: FAILURE: Voting on behalf of another account should not be allowed: expected failure, got result = "Write succeeded"Load failed
The test failed because the
voter
account name doesn't exist in thevotes
table keys and the candidate exists, so the number of votes for the candidate is incremented. You need to make sure that the signer of the transaction owns theaccount
passed to thevote
function. -
Open the
election-dapp/pact/election.pact
file in the code editor on your computer. -
Define the
ACCOUNT-OWNER
capability to enforce the guard of the account passed to thevote
function:(use coin [ details ]) (defcap ACCOUNT-OWNER (account:string) (enforce-guard (at 'guard (coin.details account))))
(use coin [ details ]) (defcap ACCOUNT-OWNER (account:string) (enforce-guard (at 'guard (coin.details account))))
This code uses the
coin.details
function to get the guard for an account by account name. Thedetails
function of thecoin
module must be imported into theelection
module to be able to use it. In this case,voter-keyset
is the guard for the account. By enforcing this guard, you can ensure that the keyset used to sign thevote
transaction belongs to the account name passed to the function. -
Apply the capability by wrapping the
update
andinsert
statements in thevote
function inside awith-capability
statement as follows:(defun vote (account:string candidateKey:string) (let ((double-vote (account-voted account))) (enforce (= double-vote false) "Multiple voting not allowed")) (with-default-read candidates candidateKey { "name": "", "votes": 0 } { "name" := name, "votes" := numberOfVotes } (enforce (> (length name) 0) "Candidate does not exist") (with-capability (ACCOUNT-OWNER account) (update candidates candidateKey { "votes": (+ numberOfVotes 1) }) (insert votes account { "candidateKey": candidateKey }) ) ))
(defun vote (account:string candidateKey:string) (let ((double-vote (account-voted account))) (enforce (= double-vote false) "Multiple voting not allowed")) (with-default-read candidates candidateKey { "name": "", "votes": 0 } { "name" := name, "votes" := numberOfVotes } (enforce (> (length name) 0) "Candidate does not exist") (with-capability (ACCOUNT-OWNER account) (update candidates candidateKey { "votes": (+ numberOfVotes 1) }) (insert votes account { "candidateKey": candidateKey }) ) ))
-
Execute the transaction using the
pact
command-line program:pact voting.repl --trace
pact voting.repl --trace
You should see that the transaction succeeds with output similar to the following:
voting.repl:44:3:Trace: Begin Tx 8: Vote on behalf of another accountvoting.repl:45:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:46:2:Trace: Expect failure: success: Voting on behalf of another account should not be allowedvoting.repl:51:0:Trace: Commit Tx 8: Vote on behalf of another accountLoad successful
voting.repl:44:3:Trace: Begin Tx 8: Vote on behalf of another accountvoting.repl:45:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionvoting.repl:46:2:Trace: Expect failure: success: Voting on behalf of another account should not be allowedvoting.repl:51:0:Trace: Commit Tx 8: Vote on behalf of another accountLoad successful
With these changes, the administrative account can't vote on behalf of another account.
Verify voting on one's own behalf
To verify that the voter account can vote on its own behalf:
-
Open the
election-dapp/pact/voting.repl
file in the code editor on your computer. -
Add a transaction to verify that the
voter
account can vote on its own behalf, leading to an increase of the number of votes onCandidate A
to 2:(env-sigs [{ 'key : "voter" , 'caps : [] }]) (begin-tx "Vote as voter") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (vote "voter" "1") (expect "Candidate A has 2 votes" 2 (at 'votes (at 0 (list-candidates))) )(commit-tx)
(env-sigs [{ 'key : "voter" , 'caps : [] }]) (begin-tx "Vote as voter") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (vote "voter" "1") (expect "Candidate A has 2 votes" 2 (at 'votes (at 0 (list-candidates))) )(commit-tx)
Remember to replace the namespace with your own principal namespace.
-
Execute the transaction using the
pact
command-line program:pact voting.repl -t
pact voting.repl -t
You should see that the transaction succeeds with output similar to the following:
voting.repl:62:4:Trace: Expect: success: Candidate A has 2 votesvoting.repl:67:2:Trace: Commit Tx 9: Vote as voterLoad successful
voting.repl:62:4:Trace: Expect: success: Candidate A has 2 votesvoting.repl:67:2:Trace: Commit Tx 9: Vote as voterLoad successful
Impressive!
You now have a simple smart contract with the basic functionality for conducting an election that allows Kadena account holders to vote on the candidate of their choice.
With these changes, you're ready to upgrade the election
module on the development network.
Update the development network
Now that you've updated and tested your election
module using the Pact REPL, you can update the module deployed on the local development network.
To update the election
module on the development network:
-
Verify the development network is currently running on your local computer.
-
Open and unlock the Chainweaver desktop or web application and verify that:
- You're connected to development network (devnet) from the network list.
- Your administrative account name with the k: prefix exists on chain 1.
- Your administrative account name is funded with KDA on chain 1.
You're going to use Chainweaver to sign the transaction that updates the
election
module. -
Open the
election-dapp/snippets
folder in a terminal shell on your computer. -
Deploy your election module on the development network by running a command similar to the following with your administrative account name:
npm run deploy-module:devnet -- k:<your-public-key> upgrade init-votes
npm run deploy-module:devnet -- k:<your-public-key> upgrade init-votes
Remember that
k:<your-public-key>
is the default account name for the administrative account that you funded in Add an administrator account. You can copy this account name from Chainweaver when viewing the account watch list.In addition to the account name and
upgrade
, you must includeinit-votes
in the command to add{"init-votes": true}
to the transaction data. This field is required to allow you to execute the(create-table votes)
statement from yourelection
module. -
Click Sign All in Chainweaver to sign the request.
After you click Sign All, the transaction is executed and the results are displayed in your terminal shell. For example, you should see output similar to the following:
{ status: 'success', data: [ 'TableCreated' ] }
{ status: 'success', data: [ 'TableCreated' ] }
-
Verify your contract changes in the Chainweaver Module Explorer by refreshing the list of Deployed Contracts, then clicking View for the
election
module.After you click View, you should see the updated list of functions and capabilities. If you click Open, you can view the module code in the editor panel and verify that the
election
module deployed on the local development network is what you expect.
Update the frontend
As you learned in Nominate candidates, the election application frontend is written in TypeScript and uses repositories to exchange data with the backend.
By default, the frontend uses the in-memory implementations of the repositories.
By making changes to the implementation of the interface IVoteRepository
in the
frontend/src/repositories/candidate/DevnetVoteRepository.ts
file, you can configure the
frontend to use the devnet
backend instead of the in-memory
backend.
After making these changes, you can use the frontend to cast votes on candidates listed in the candidates
table and managed by the election
module running on the development network blockchain.
To update the frontend to use the election
module:
-
Open
election-dapp/frontend/src/repositories/candidate/DevnetVoteRepository.ts
in your code editor. -
Replace the value of the
NAMESPACE
constant with your own principal namespace.const NAMESPACE = 'n_14912521e87a6d387157d526b281bde8422371d1';
const NAMESPACE = 'n_14912521e87a6d387157d526b281bde8422371d1';
-
Review the
hasAccountVoted
function:const hasAccountVoted = async (account: string): Promise<boolean> => { const transaction = Pact.builder // @ts-ignore .execution(Pact.modules[`${NAMESPACE}.election`]['account-voted'](account)) .setMeta({ chainId: CHAIN_ID }) .setNetworkId(NETWORK_ID) .createTransaction(); const { result } = await client.dirtyRead(transaction); if (result.status === 'success') { return result.data.valueOf() as boolean; } else { console.log(result.error); return false; }};
const hasAccountVoted = async (account: string): Promise<boolean> => { const transaction = Pact.builder // @ts-ignore .execution(Pact.modules[`${NAMESPACE}.election`]['account-voted'](account)) .setMeta({ chainId: CHAIN_ID }) .setNetworkId(NETWORK_ID) .createTransaction(); const { result } = await client.dirtyRead(transaction); if (result.status === 'success') { return result.data.valueOf() as boolean; } else { console.log(result.error); return false; }};
-
Remove the
@ts-ignore
comment from the function and notice the resulting errors. To fix the errors, you must generate types for your Pact module that can be picked up by@kadena/client
. -
Open a terminal, change to the
election-dapp/frontend
directory, then generate types for yourelection
module by running the following command:npm run pactjs:generate:contract:election
npm run pactjs:generate:contract:election
This command uses the
pactjs
library to generate the TypeScript definitions for the election contract and should clear the errors reported by the code editor. Depending on the code editor, you might need to close the project in the editor and reopen it to reload the code editor window with the change. -
Review the
vote
function, remove the@ts-ignore
comment, and save your changes to theDevnetVoteRepository.ts
file. -
Open the
election-dapp/frontend
folder in a terminal shell on your computer. -
Install the frontend dependencies by running the following command:
npm install
npm install
-
Start the frontend application configured to use the development network running locally by running the following command:
npm run start-devnet
npm run start-devnet
Cast a vote
Now that you have deployed the smart contract on the development network and updated the frontend to use the election module backend, you can use the election application to cast votes.
To cast a vote using the election application website:
-
Verify the development network is currently running on your local computer.
-
Open and unlock the Chainweaver desktop or web application and verify that:
- You're connected to development network (devnet) from the network list.
- Your administrative account name with the k: prefix exists on chain 1.
- Your administrative account name is funded with KDA on chain 1.
You're going to use Chainweaver to sign the voting transaction.
-
Open
http://localhost:5173
in your browser, then click Set Account. -
Paste your administrative account, then click Save.
-
Click Add Candidate to add candidates, if necessary.
-
Click Vote Now for a candidate row.
-
Sign the transaction, and wait for the transaction to finish.
-
Verify that the number of votes for the candidate you voted for increased by one vote.
After you vote, the Vote Now button is disabled because the frontend checks if your account has already voted by making a
local
request to theaccount-voted
function of theelection
Pact module.
Next steps
In this tutorial, you learned how to:
- Organize test cases into separate REPL files.
- Modify the
vote
function iteratively using test cases and expected results. - Use the
with-default-read
function. - Add a
votes
database table to store the vote cast by each account holder. - Connect the voting functionality from the frontend to the development network as a backend.
With this tutorial, you completed the functional requirements for the election
Pact module, deployed it as a smart contract on your local development network, and interacted with the blockchain backend through the frontend of the election application website.
However, you might have noticed that your administrative account had to pay for gas to cast a vote.
To make the election accessible, account holders should be able to cast a vote without having to pay transaction fees.
The next tutorial demonstrates how to add a gas station module to the election
smart contract.
With this module, an election organization can act as the owner of an account that provides funds to pay the transaction fees on behalf of election voters.
By using a gas station, voters can cast votes without incurring any transaction fees.
To see the code for the activity you completed in this tutorial and get the starter code for the next tutorial, check out the 09-gas-station
branch from the election-dapp
repository by running the following command in your terminal shell:
git checkout 09-gas-station
git checkout 09-gas-station