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 election-dapp repository 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 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/pactfolder in the code editor on your computer. -
Rename
election.repltocandidates.repl. -
Create a new
setup.replfile in thepactfolder. -
Move the code before
(begin-tx "Load election module")from thecandidates.replinto thesetup.replfile. -
Create a new
voting.replfile in thepactfolder and add the following as the first line in the file:(load "setup.repl")(load "setup.repl") -
Open the
candidates.replfile and and add the following as the first line in the file:(load "setup.repl")(load "setup.repl") -
Verify tests in the
candidates.replfile still pass by running the following command:pact candidates.repl -tpact candidates.repl -t -
Verify that
voting.replloads successfully by running the following command:pact voting.repl -tpact voting.repl -t
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.replfile in the code editor on your computer. -
Add transactions to load the
electionPact module and to add a candidate to thecandidatestable:(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 for 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
votefunction 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
votefunction doesn't exist yet in theelectionmodule and you would see output similar to the following:voting.repl:18:5:Error: Cannot resolve voteLoad failedvoting.repl:18:5:Error: Cannot resolve voteLoad failed -
Open the
election-dapp/pact/election.pactfile in your code editor. -
Define the
votefunction after theadd-candidatefunction and before thecandidates-schemadefinition 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
votefunction takes thecandidateKeyparameter with a of type string:- The
candidateKeyvalue specifies the key for the row in thecandidatestable to read using the built-inwith-readPact function. - The database column named
"votes"is assigned a value from thenumberOfVotesvariable.
The
votefunction then calls the built-inupdatePact 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
votefunction only updates thevotescolumn. The new value is the current number of votes that was obtained fromwith-readand stored in thenumberOfVotesvariable incremented by one ((+ numberOfVotes 1)).
- The
-
Execute the transaction using the
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou 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 successfulvoting.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 passed in that doesn't exist in the database.
To prepare a test for votes on an invalid candidate:
-
Open the
election-dapp/pact/voting.replfile in the code editor on your computer. -
Add the following transaction before the
Voting for a candidatetransaction:(begin-tx "Voting for a non-existing candidate") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Cannot vote for a non-existing candidate" (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" (vote "X") )(commit-tx)Remember to replace the namespace with your own principal namespace.
-
Execute the transaction using the
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou 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 candidatevoting.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 candidateThe test returns the expected result—failure—because the call to
with-readfails for thecandidateKeyvalue of"X". The failure prevents the execution of theupdatefunction.As you add checks to the
votefunction, 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-failureis 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
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou 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 candidatevoting.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 candidateTo prevent the read operation from failing with a standard message, you can use the built-in
with-default-readPact function. Thewith-default-readfunction doesn't throw an error if no row is found with the specified key, but returns a default object instead. The default object contains the default values for the name ("") and votes (0) columns.For successful reads, the value of the
"name"column is assigned to anamevariable, similar to the value of the"votes"column. This allows you to enforce thatnamemust not be an empty string, and throw a specific error if it is. -
Open the
election-dapp/pact/election.pactfile in your code editor. -
Update the
votefunction to use thewith-default-readfunction and return an error ifnameis an empty string:(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) }) )) -
Execute the transaction using the
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou 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:18:0:Trace: Commit Tx 4: Voting for a non-existing candidatevoting.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:18:0:Trace: Commit Tx 4: Voting for a non-existing candidateThe
votefunction now returns a specific error message when someone tries to vote for a candidate that doesn't exist.
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.pactfile in your code editor. -
Add the schema for the
votesdatabase table inside of theelectionmodule definition after the definition of thecandidatesschema 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 theelectionmodule definition and theinit-candidatescode snippet:(if (read-msg "init-votes") [(create-table votes)] [])(if (read-msg "init-votes") [(create-table votes)] [])With this code,
read-msgreads theinit-votesfield from the transaction data. If you set this field totruein your module deployment transaction, the statement between the first square brackets is executed. This statement creates thevotestable 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.replfile in your code editor. -
Add
, 'init-votes: trueto theenv-dataso that this data is loaded in the Pact REPL environment when you load theelectionmodule and thevotestable 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
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou should see that the transaction succeeds with
TableCreatedtwice 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.replfile 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
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou 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 failedvoting.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 failedRemember that all transactions in
voting.replare signed with theadmin-keysetyou defined for the REPL environment in thesetup.replfile. 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
votefunction andelectionmodule. -
Open the
election-dapp/pact/election.pactfile in your code editor. -
Update the
votefunction 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
votefunction. - Stores the result from a new
account-votedfunction in thedouble-votevariable and uses that value to prevent an account from voting more than once. - Enforces that no row in the
votestable is keyed with the account name using thewith-default-readpattern that you used to prevent voting on a non-existent candidate. - Inserts a new row into the
votestable with the account name as the key and the candidate key as the value for thecandidateKeycolumn every time thevotefunction is called.
- Adds the account of the voter as the first parameter of the
-
Add the
account-votedfunction 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-votedfunction to determine if Vote Now should be enabled. -
Open the
election-dapp/pact/voting.replfile in the code editor on your computer. -
Update all calls to the
votefunction to pass your administrative account name as the first argument.For example, update the
votefunction in theDouble votetransaction:(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
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou 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 successfulvoting.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 successfulWith these changes, the same account can't call the
votefunction 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.replfile in the code editor on your computer. -
Add a
voter-keysettoenv-dataso that this data is loaded in the Pact REPL environment when you load theelectionmodule:, 'voter-keyset: { "keys": ["voter"], "pred": "keys-all" }, 'voter-keyset: { "keys": ["voter"], "pred": "keys-all" } -
Load the
coinmodule 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-tableandcoin.allocation-tablerequired to create thevoteraccount. - Creates the
voteraccount and your administrative account in thecoinmodule database.
Remember to replace the administrative account name with your own account name.
- Creates the
-
Open the
election-dapp/pact/voting.replfile in the code editor on your computer. -
Add a transaction at the end of the file to cast a vote on behalf of the
voteraccount 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
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou 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 failedvoting.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 failedThe test failed because the
voteraccount name doesn't exist in thevotestable 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 the KDA account passed to thevotefunction. -
Open the
election-dapp/pact/election.pactfile in the code editor on your computer. -
Define the
ACCOUNT-OWNERcapability to enforce the guard of the account passed to thevotefunction:(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.detailsfunction to get the guard for an account by account name. Thedetailsfunction of thecoinmodule must be imported into theelectionmodule to be able to use it. In this case,voter-keysetis the guard for the account. By enforcing this guard, you can ensure that the keyset used to sign thevotetransaction belongs to the account name passed to the function. -
Apply the capability by wrapping the
updateandinsertstatements in thevotefunction inside awith-capabilitystatement 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
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou 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 successfulvoting.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 successfulWith 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.replfile in the code editor on your computer. -
Add a transaction to verify that the
voteraccount can vote on its own behalf, leading to an increase of the number of votes onCandidate Ato 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
pactcommand-line program:pact voting.repl -tpact voting.repl -tYou 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 successfulvoting.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
electionmodule. -
Open the
election-dapp/snippetsfolder 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-votesnpm run deploy-module:devnet -- k:<your-public-key> upgrade init-votesRemember 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-votesin 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 yourelectionmodule. -
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
electionmodule.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 pane and verify that the
electionmodule deployed on the local development network is what you expect.
Update the frontend and cast a vote
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
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 cast a vote using the election application website:
-
Open
election-dapp/frontend/src/repositories/candidate/DevnetVoteRepository.tsin your code editor. -
Replace the value of the
NAMESPACEconstant with your own principal namespace.const NAMESPACE = 'n_14912521e87a6d387157d526b281bde8422371d1';const NAMESPACE = 'n_14912521e87a6d387157d526b281bde8422371d1'; -
Review the
hasAccountVotedfunction: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-ignorecomment 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/frontenddirectory, then generate types for yourelectionmodule by running the following command:npm run pactjs:generate:contract:electionnpm run pactjs:generate:contract:electionThis command uses the
pactjslibrary 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
votefunction, remove the@ts-ignorecomment, and save your changes to theDevnetVoteRepository.tsfile. -
Open the
election-dapp/frontendfolder in a terminal shell on your computer. -
Install the frontend dependencies by running the following command:
npm installnpm install -
Start the frontend application configured to use the
devnetbackend by running the following command:npm run start-devnetnpm run start-devnet -
Open
http://localhost:5173in your browser, then click Set Account. -
Paste your administrative account, then click Save.
-
Add a candidate, if necessary.
-
Click Vote Now for a candidate, 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
localrequest to theaccount-votedfunction of theelectionPact module.
View the result after voting
Next steps
In this tutorial, you learned how to:
- Organize test cases into separate REPL files.
- Modify the
votefunction iteratively using test cases and expected results. - Use the
with-default-readfunction. - Add a
votesdatabase 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-stationgit checkout 09-gas-station