Nominate candidates
In Write a smart contract, you learned about defining Pact modules and created a skeleton election module for the smart contract that will become the backend of the election application.
In this tutorial, you'll update the election module with a database table and functions to support the following operations:
- Store a list of candidates and the number of votes each candidate receives.
- Store a list of the accounts that have voted to ensure that every account can vote only once.
- Add nominated candidates to the candidate table.
- List all of the candidates that are stored in the table.
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 a minimal election module using the Pact smart contract language as described in Write a smart contract.
Define the database schema and table
To prepare the election module database, you must first define a schema
for the table. You can then define a table that uses the schema inside the
election module. The actual creation of the table happens outside the Pact
module, just like selecting the namespace.
To define the database schema and table:
-
Open the
election-dapp/pact/election.pactfile in the code editor on your computer. -
Add the schema for the database table inside of the
electionmodule definition with the following lines of code:(defschema candidates-schema "Candidates table schema" name:string votes:integer) (deftable candidates:{candidates-schema})(defschema candidates-schema "Candidates table schema" name:string votes:integer) (deftable candidates:{candidates-schema})In this code,
defschemadefines acandidate-schemafor a database table with two columns:nameof type string andvotesof type integer. -
Create the table outside of the election module by adding the following lines of code at the end of the
./pact/election.pactfile, after the closing parenthesis ()) of theelectionmodule definition:(if (read-msg "init-candidates") [(create-table candidates)] [])(if (read-msg "init-candidates") [(create-table candidates)] [])With this code, the
read-msgfunction reads theinit-candidatesfield from the transaction data. If you set this field totruein the data for your module deployment transaction, the statement between the first square brackets—(create-table candidates)—is executed to create thecandidatestable based on its schema definition inside theelectionmodule.
Test table creation
Before trying to create the table on your local development network, you can verify that your changes work as expected by running some tests in the Pact REPL.
To test table creation:
-
Open the
election-dapp/pactfolder in the code editor on your computer. -
Create a new file named
election.replin thepactfolder. -
Set the
env-dataandenv-sigsfields for the REPL test environment to use the public key for your own administrative account.For example:
(env-data { 'admin-keyset: { 'keys : [ "5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" ] , 'pred : 'keys-all } , 'upgrade: false , 'init-candidates: true }) (env-sigs [{ 'key : "5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" , 'caps : [] }])(env-data { 'admin-keyset: { 'keys : [ "5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" ] , 'pred : 'keys-all } , 'upgrade: false , 'init-candidates: true }) (env-sigs [{ 'key : "5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0" , 'caps : [] }])Also, notice that
'init-candidates: trueis included in the environment data to ensure that the(create-table candidates)command is executed when you load theelectionmodule into the Pact REPL. -
Define your principal namespace and the
admin-keysetfor the namespace using the principal namespace you used in yourelection.pactfile.(begin-tx "Define principal namespace") (define-namespace 'n_14912521e87a6d387157d526b281bde8422371d1 (read-keyset 'admin-keyset ) (read-keyset 'admin-keyset ))(commit-tx) (begin-tx "Define admin-keyset") (namespace 'n_14912521e87a6d387157d526b281bde8422371d1) (define-keyset "n_14912521e87a6d387157d526b281bde8422371d1.admin-keyset" (read-keyset 'admin-keyset ))(commit-tx)(begin-tx "Define principal namespace") (define-namespace 'n_14912521e87a6d387157d526b281bde8422371d1 (read-keyset 'admin-keyset ) (read-keyset 'admin-keyset ))(commit-tx) (begin-tx "Define admin-keyset") (namespace 'n_14912521e87a6d387157d526b281bde8422371d1) (define-keyset "n_14912521e87a6d387157d526b281bde8422371d1.admin-keyset" (read-keyset 'admin-keyset ))(commit-tx)These transactions are required because, inside
election.pactfile, theelectionmodule is defined in your principal namespace and it is governed by theadmin-keysetin that namespace. -
Add a transaction to load the election module:
(begin-tx "Load election module") (load "election.pact")(commit-tx)(begin-tx "Load election module") (load "election.pact")(commit-tx) -
Execute the transaction using the
pactcommand-line program running locally or using pact-cli from the Docker container.If
pact-cliis installed locally, run the following command inside thepactfolder in current terminal shell:pact election.repl -tpact election.repl -tAs before, if you don't have
pactinstalled locally, you can load theelection.replfile with the following command:(load "election.repl")(load "election.repl")If you are using the
pact-cliin a browser, you can replace thepact election.repl -tcommand with(load "election.repl")throughout this tutorial.You should see that the transaction succeeds with output similar to the following:
election.pact:3:0:Trace: Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election, hash TW9dmlTaCle12OF9zwn9Z_oF1cX2qhTbZYZAwDXkTqYelection.pact:16:0:Trace: ["TableCreated"]election.repl:27:0:Trace: Commit Tx 2: Load election moduleLoad successfulelection.pact:3:0:Trace: Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election, hash TW9dmlTaCle12OF9zwn9Z_oF1cX2qhTbZYZAwDXkTqYelection.pact:16:0:Trace: ["TableCreated"]election.repl:27:0:Trace: Commit Tx 2: Load election moduleLoad successful
List candidates from a table
Although the candidates table seems to have been created successfully, it is
worth testing that the table works as expected before updating the election
module on the development network.
To test that the table works as expected:
-
Open the
election-dapp/pact/election.replfile in the code editor on your computer. -
Add a transaction to test the current implementation of the
election.list-candidatesfunction:(begin-tx "List candidates") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect "There should be no candidates in the candidates table" [1, 2, 3, 4, 5] (list-candidates) )(commit-tx)(begin-tx "List candidates") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect "There should be no candidates in the candidates table" [1, 2, 3, 4, 5] (list-candidates) )(commit-tx) -
Execute the transaction using the
pactcommand-line program:pact election.repl -tpact election.repl -tIf the current implementation of the
list-candidatesfunction returns [1, 2, 3, 4, 5], you should see the transaction succeed with output similar to the following:election.repl:29:0:Trace: Begin Tx 3: List candidateselection.repl:30:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:31:2:Trace: Expect: success: There should be no candidates in the candidates tableelection.repl:36:0:Trace: Commit Tx 3: List candidatesLoad successfulelection.repl:29:0:Trace: Begin Tx 3: List candidateselection.repl:30:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:31:2:Trace: Expect: success: There should be no candidates in the candidates tableelection.repl:36:0:Trace: Commit Tx 3: List candidatesLoad successfulIf you were to change the expected output to an empty list (
[]) and run the file again, you would see the transaction fails with output similar to the following:election.repl:37:0:Trace: Commit Tx 3: List candidateselection.repl:32:2:ExecError: FAILURE: There should be no candidates in the candidates table: expected []:[<a>], received [1 2 3 4 5]:[<c>]Load failedelection.repl:37:0:Trace: Commit Tx 3: List candidateselection.repl:32:2:ExecError: FAILURE: There should be no candidates in the candidates table: expected []:[<a>], received [1 2 3 4 5]:[<c>]Load failedYou can fix this issue by updating the return value of the
list-candidatesfunction in theelection-dapp/pact/election.pactfile. -
Open the
election-dapp/pact/election.pactfile in your code editor. -
Update the return value of the
list-candidatesfunction to select all of the rows of thecandidatestable, including the key and the column values of each row.For example:
(defun list-candidates () (fold-db candidates (lambda (key columnData) true) (lambda (key columnData) (+ { "key": key } columnData)) ))(defun list-candidates () (fold-db candidates (lambda (key columnData) true) (lambda (key columnData) (+ { "key": key } columnData)) ))In this code, the
fold-dbfunction is like aSELECT * FROM tablestatement in SQL, except that it fetches the value of thekeycolumn separately from the other column values.- The first argument for
fold-dbis the table name. - The second argument is a predicate function that determines which rows should be selected.
To fetch all rows from a table, you can simply return
truehere. - The third argument is an accumulator function that allows you to map the data of each row to a different format.
This example formats the return value of the
fold-dbfunction as a JSON object with the following structure.[ { "key": "1", "name": "Candidate A", "votes": 0 }, { "key": "2", "name": "Candidate B", "votes": 0 }][ { "key": "1", "name": "Candidate A", "votes": 0 }, { "key": "2", "name": "Candidate B", "votes": 0 }] - The first argument for
-
Execute the transaction using the
pactcommand-line program:pact election.repl -tpact election.repl -tBecause there are no candidates in the table, you should see the transaction succeeds with output similar to the following:
election.repl:30:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:31:2:Trace: Expect: success: There should be no candidates in the candidates tableelection.repl:36:0:Trace: Commit Tx 3: List candidatesLoad successfulelection.repl:30:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:31:2:Trace: Expect: success: There should be no candidates in the candidates tableelection.repl:36:0:Trace: Commit Tx 3: List candidatesLoad successfulNote that you shouldn't include a call to a function like
fold-dbin transactions sent to the blockchain. Instead, you can make a local request to select all rows from a table to save gas. You'll learn more about making local requests using the Kadena client later in this tutorial.
Add candidates
At this point, you have a database table for storing candidate names and the votes they've received, but without any candidates for anyone to vote on.
To add candidates to the database:
-
Open the
election-dapp/pact/election.replfile in your code editor. -
Add a transaction to test that candidates have been added to the database using the
election.add-candidatefunction:(begin-tx "Add candidates") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect "Add Candidate A" "Write succeeded" (add-candidate { "key": "1", "name": "Candidate A" }) ) (expect "Add Candidate B" "Write succeeded" (add-candidate { "key": "2", "name": "Candidate B" }) ) (expect "Add Candidate C" "Write succeeded" (add-candidate { "key": "3", "name": "Candidate C" }) )(commit-tx)(begin-tx "Add candidates") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect "Add Candidate A" "Write succeeded" (add-candidate { "key": "1", "name": "Candidate A" }) ) (expect "Add Candidate B" "Write succeeded" (add-candidate { "key": "2", "name": "Candidate B" }) ) (expect "Add Candidate C" "Write succeeded" (add-candidate { "key": "3", "name": "Candidate C" }) )(commit-tx)If you were to execute the transaction now, the test would fail because the
add-candidatefunction doesn't exist yet in theelectionmodule and you would see output similar to the following:election.repl:40:0:Error: Cannot resolve add-candidateLoad failedelection.repl:40:0:Error: Cannot resolve add-candidateLoad failedHowever, from this code, you can see that the
add-candidatefunction accepts a candidate object as an argument, and that the object is defined in JSON format.Notice that this object has the fields
keyandname, while thecandidate-schemayou defined for thecandidatestable has two columnsnameandvotes. Thevotescolumn always has an initial value of0when a new candidate is added, so you don't need to send a value for votes in the transaction.The
keyvalue is a unique index for the table row that is added. This value can't be automatically generated, so you have to pass a value yourself. -
Open the
election-dapp/pact/election.pactfile in your code editor. -
Define the
add-candidatefunction inside the election module definition to receive acandidateJSON object and call the built-ininsertfunction:(defun add-candidate (candidate) (insert candidates (at 'key candidate) { "name": (at 'name candidate), "votes": 0 } ))(defun add-candidate (candidate) (insert candidates (at 'key candidate) { "name": (at 'name candidate), "votes": 0 } ))In this code, you pass the following arguments to the
insertfunction:- The name of table you want to update.
In this case, the table is the
candidatestable. - The value for the key of the row to be inserted.
In this case, the value of the
keyfield is extracted from thecandidateobject - The key-value object representing the row to be inserted into the table.
The keys correspond to the column names.
In this case, the
votescolumn of the new value always gets a value0and thenamecolumn gets a value of"Candidate A","Candidate B", or"Candidate C", as per your test cases.
pact election.repl -tpact election.repl -tYou should see that the transaction succeeds with output similar to the following:
election.repl:39:0:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:40:0:Trace: Expect: success: Add Candidate Aelection.repl:45:0:Trace: Expect: success: Add Candidate Belection.repl:50:0:Trace: Expect: success: Add Candidate Celection.repl:55:0:Trace: Commit Tx 4: Add candidatesLoad successfulelection.repl:39:0:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:40:0:Trace: Expect: success: Add Candidate Aelection.repl:45:0:Trace: Expect: success: Add Candidate Belection.repl:50:0:Trace: Expect: success: Add Candidate Celection.repl:55:0:Trace: Commit Tx 4: Add candidatesLoad successfulThe key of each row in a table must be unique. You can add a transaction to the
election.replfile to test that you can't insert a row with a duplicate key. For example:(begin-tx "Add candidate with existing key") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Database exception: Insert: row found for key 1" (add-candidate { "key": "1", "name": "Candidate D" }) )(commit-tx)(begin-tx "Add candidate with existing key") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Database exception: Insert: row found for key 1" (add-candidate { "key": "1", "name": "Candidate D" }) )(commit-tx)If you were to execute this transaction, it would fail—as expected—with output similar to the following:
election.repl:57:0:Trace: Begin Tx 5: Add candidate with existing keyelection.repl:58:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:59:2:Trace: Expect failure: success: Database exception: Insert: row found for key 1election.repl:63:0:Trace: Commit Tx 5: Add candidate with existing keyLoad successfulelection.repl:57:0:Trace: Begin Tx 5: Add candidate with existing keyelection.repl:58:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:59:2:Trace: Expect failure: success: Database exception: Insert: row found for key 1election.repl:63:0:Trace: Commit Tx 5: Add candidate with existing keyLoad successful - The name of table you want to update.
In this case, the table is the
-
Verify that you only have three candidates in the table by adding the following assertion to the
election-dapp/pact/election.replfile:(begin-tx "List candidates") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect "There should be three candidates" 3 (length (list-candidates)) )(commit-tx)(begin-tx "List candidates") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect "There should be three candidates" 3 (length (list-candidates)) )(commit-tx) -
Execute the transaction using the
pactcommand-line program:pact election.repl -tpact election.repl -tYou should see that the transaction succeeds with output similar to the following:
election.repl:64:0:Trace: Begin Tx 6: List candidateselection.repl:65:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:66:2:Trace: Expect: success: There should be three candidateselection.repl:71:0:Trace: Commit Tx 6: List candidatesLoad successfulelection.repl:64:0:Trace: Begin Tx 6: List candidateselection.repl:65:2:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:66:2:Trace: Expect: success: There should be three candidateselection.repl:71:0:Trace: Commit Tx 6: List candidatesLoad successfulYou've now seen how candidates can be stored in a database table and that the
list-candidatesfunction works as expected to retrieve information from that table. The next step is to restrict access to theadd-candidatefunction, so that ony theelectionmodule owner can update thecandidatesdatabase.
Guard add-candidate with a capability
Right now, the add-candidate function is publicly accessible. Anyone with a
Kadena account can nominate a candidate. If everyone can nominate and vote on
anyone, the whole election process and the idea of representative governance
breaks down. To prevent that kind of chaos, you need a gatekeeper—a guard—that
restricts access to the nominating process and the number of candidates to be
voted on.
For the election application, this gatekeeper or guard is the holder of the
admin-keyset administrative account. To restrict access to the add-candidate
function, you can use the GOVERNANCE capability. The GOVERNANCE capability
enforces the use of the admin-keyset to sign transactions that call specific
functions. In the election application, the GOVERNANCE capability protects
access to the add-candidate function.
To guard access to the add-candidate function:
-
Open the
election-dapp/pact/election.replfile in the code editor on your computer. -
Add a transaction in which you expect adding a fourth candidate to fail.
(env-data { 'admin-keyset : { 'keys : [ 'other-key ] , 'pred : 'keys-all } }) (env-sigs [{ 'key : 'other-key , 'caps : [] }]) (begin-tx "Add candidate without permission") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Adding a candidate with the wrong keyset should fail" "Keyset failure (keys-all)" (add-candidate { "key": "4", "name": "Candidate D" }) )(commit-tx)(env-data { 'admin-keyset : { 'keys : [ 'other-key ] , 'pred : 'keys-all } }) (env-sigs [{ 'key : 'other-key , 'caps : [] }]) (begin-tx "Add candidate without permission") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (expect-failure "Adding a candidate with the wrong keyset should fail" "Keyset failure (keys-all)" (add-candidate { "key": "4", "name": "Candidate D" }) )(commit-tx) -
Execute the transaction using the
pactcommand-line program:pact election.repl -tpact election.repl -tYou should see that the transaction fails with output similar to the following:
election.repl:89:4:Trace: FAILURE: Adding a candidate with the wrong keyset should fail: expected failure, got result = "Write succeeded"election.repl:94:2:Trace: Commit Tx 7: Add candidate without permissionelection.repl:89:4:ExecError: FAILURE: Adding a candidate with the wrong keyset should fail: expected failure, got result = "Write succeeded"Load failedelection.repl:89:4:Trace: FAILURE: Adding a candidate with the wrong keyset should fail: expected failure, got result = "Write succeeded"election.repl:94:2:Trace: Commit Tx 7: Add candidate without permissionelection.repl:89:4:ExecError: FAILURE: Adding a candidate with the wrong keyset should fail: expected failure, got result = "Write succeeded"Load failed -
Open the
election-dapp/pact/election.pactfile in your code editor. -
Update the
add-candidatefunction to add a capability guard as follows:(defun add-candidate (candidate) (with-capability (GOVERNANCE) (insert candidates (at 'key candidate) { "name": (at 'name candidate), "votes": 0 } ) ))(defun add-candidate (candidate) (with-capability (GOVERNANCE) (insert candidates (at 'key candidate) { "name": (at 'name candidate), "votes": 0 } ) ))The
with-capabilityfunction tries to bring theGOVERNANCEin scope of the code block that it wraps. If it fails to do so, because of a keyset failure in this case, the wrapped code block isn't executed. -
Execute the transaction using the
pactcommand-line program:pact election.repl -tpact election.repl -tYou should see output similar to the following that verifies the
add-candidatefunction is now guarded by theGOVERNANCEcapability:election.repl:87:2:Trace: Begin Tx 7: Add candidate without permissionelection.repl:88:4:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:89:4:Trace: Expect failure: success: Adding a candidate with the wrong keyset should failelection.repl:94:2:Trace: Commit Tx 7: Add candidate without permissionLoad successfulelection.repl:87:2:Trace: Begin Tx 7: Add candidate without permissionelection.repl:88:4:Trace: Using n_14912521e87a6d387157d526b281bde8422371d1.electionelection.repl:89:4:Trace: Expect failure: success: Adding a candidate with the wrong keyset should failelection.repl:94:2:Trace: Commit Tx 7: Add candidate without permissionLoad successful
Update the election module locally
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 module.
-
Open the
election-dapp/snippetsfolder in the terminal shell. -
Update your
electionmodule 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-candidatesnpm run deploy-module:devnet -- k:<your-public-key> upgrade init-candidatesRemember 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, you pass
upgradeandinit-candidatesto add{"init-candidates": true, "upgrade": true}to the transaction data. These fields are required to allow you to update the module and execute the(create-table candidates)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:
{ gas: 60855, result: { status: 'success', data: [ 'TableCreated' ] }, reqKey: 'Bd80eOQ-yeWqrcsj6iEuFZ2rcMrv3OsWXhGZKOyEkHw', logs: 'UjxuW6e-d_p2nmYsytoqdKjqza3Gq_839IIWZ1uQDDs', events: [ { params: [Array], name: 'TRANSFER', module: [Object], moduleHash: 'M1gabakqkEi_1N8dRKt4z5lEv1kuC_nxLTnyDCuZIK0' } ], metaData: { publicMeta: { creationTime: 1705172812, ttl: 28800, gasLimit: 100000, chainId: '1', gasPrice: 1e-8, sender: 'k:5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0' }, blockTime: 1705172951004717, prevBlockHash: 'AMJ6IoPnkMJ8WZFe6Vso5uuYAhep4gP87ANb7rYyvwg', blockHeight: 14456 }, continuation: null, txId: 14483, preflightWarnings: []}{ status: 'success', data: [ 'TableCreated' ] }{ gas: 60855, result: { status: 'success', data: [ 'TableCreated' ] }, reqKey: 'Bd80eOQ-yeWqrcsj6iEuFZ2rcMrv3OsWXhGZKOyEkHw', logs: 'UjxuW6e-d_p2nmYsytoqdKjqza3Gq_839IIWZ1uQDDs', events: [ { params: [Array], name: 'TRANSFER', module: [Object], moduleHash: 'M1gabakqkEi_1N8dRKt4z5lEv1kuC_nxLTnyDCuZIK0' } ], metaData: { publicMeta: { creationTime: 1705172812, ttl: 28800, gasLimit: 100000, chainId: '1', gasPrice: 1e-8, sender: 'k:5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0' }, blockTime: 1705172951004717, prevBlockHash: 'AMJ6IoPnkMJ8WZFe6Vso5uuYAhep4gP87ANb7rYyvwg', blockHeight: 14456 }, continuation: null, txId: 14483, preflightWarnings: []}{ 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, the Module Explorer displays the
list-candidatesandadd-candidatefunctions. If you click Open, you can view the module code in the editor pane and verify that theelectionmodule deployed on the local development network is what you expect.
Connect the front-end
You now have the election backend defined in a smart contract running on the development network. To make the functions in the smart contract available to the election application website, you need to modify the frontend to exchange data with the development network.
The frontend, written in TypeScript, uses repositories to exchange data with the
backend. The interfaces for these repositories are defined in the
frontend/src/types.ts file. By default, the frontend uses the in-memory
implementations of the repositories. By making changes to the implementation of
the interface ICandidateRepository in
frontend/src/repositories/candidate/DevnetCandidateRepository.ts, 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 add candidates
to and list candidates from the candidates table managed by your election
module running on the development network blockchain.
List candidates
To modify the frontend to list candidates from the development network:
-
Open
election-dapp/frontend/src/repositories/candidate/DevnetCandidateRepository.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
listCandidatesfunction:const listCandidates = async (): Promise<ICandidate[]> => { const transaction = Pact.builder // @ts-ignore .execution(Pact.modules[`${NAMESPACE}.election`]['list-candidates']()) .setMeta({ chainId: CHAIN_ID, gasLimit: 100000, }) .setNetworkId(NETWORK_ID) .createTransaction(); const { result } = await client.dirtyRead(transaction); return result.status === 'success' ? (result.data.valueOf() as ICandidate[]) : [];};const listCandidates = async (): Promise<ICandidate[]> => { const transaction = Pact.builder // @ts-ignore .execution(Pact.modules[`${NAMESPACE}.election`]['list-candidates']()) .setMeta({ chainId: CHAIN_ID, gasLimit: 100000, }) .setNetworkId(NETWORK_ID) .createTransaction(); const { result } = await client.dirtyRead(transaction); return result.status === 'success' ? (result.data.valueOf() as ICandidate[]) : [];}; -
Remove the
@ts-ignorecomment and notice that the name of your module cannot be found inPact.modules.To fix this problem, you must generate types for your Pact module that can be picked up by the Kadena client (
@kadena/clientlibrary). -
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 error 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.After you clear the error, note that the
listCandidatesfunction:- Sets the chain identifier, gas limit, and network identifier before creating the transaction.
- Uses the
dirtyReadmethod to preview the transaction result without sending a transaction to the blockchain. ThedirtyReadmethod is provided by the Kadena client library. This method allows you to return a raw response for a transaction as you saw when you deployed your smart contract. - Processes the response from the development network and returns a list of candidates or an empty list.
-
Change to the terminal where the
election-dapp/frontenddirectory is your current working directory. -
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 and verify that the website loads without errors.You'll notice that—unlike the frontend configured to the in-memory backend—there are no candidates displayed when the frontend connects to the development network backend. With the development network backend, candidates must be added to the
candidatestable before they can be displayed. To do that, you must first modify theaddCandidatefunction in the frontend.
Add candidate
To modify the frontend to add candidates from the development network:
-
Open
election-dapp/frontend/src/repositories/candidate/DevnetCandidateRepository.tsin your code editor. -
Review the
addCandidatefunction.In the first line, the function receives a candidate object and the account of the transaction sender.
const addCandidate = async (candidate: ICandidate, sender: string = ''): Promise<void> => {const addCandidate = async (candidate: ICandidate, sender: string = ''): Promise<void> => {You provide this information using a form on the website.
The next lines start constructing the transaction:
const transaction = Pact.builder.execution( // @ts-ignore Pact.modules[`${NAMESPACE}.election`]['add-candidate'](candidate),);const transaction = Pact.builder.execution( // @ts-ignore Pact.modules[`${NAMESPACE}.election`]['add-candidate'](candidate),); -
Remove the
@ts-ignorecomment to enable the frontend function to call theadd-candidatefunction in yourelectionmodule.The function takes the
candidateobject to insert data into thecandidatesdatabase when the transaction is executed.Because the
add-candidatefunction is guarded by theGOVERNANCEcapability that enforces theadmin-keysetaccount, the next lines add the keyset and signer data to the transaction:.addData('admin-keyset', { keys: [accountKey(sender)], pred: 'keys-all',}).addSigner(accountKey(sender)).addData('admin-keyset', { keys: [accountKey(sender)], pred: 'keys-all',}).addSigner(accountKey(sender))These lines correspond to the
(env-data)and(env-sig)code you specified in your./pact/election.replfile. Unlike the transaction for listing candidates, the transaction for adding candidates must be sent to the blockchain, so you must pay a transaction fee—in units of gas—for the resources consumed to process the transaction.The value of the
senderAccountfield in the metadata specifies the account that pays for gas. This is important to remember because, in the Add a gas station tutorial, you'll specify the account of a gas station to pay for transactions that are signed by voters. However, the transaction to add a candidate must be signed and paid for by the same account..addSigner(accountKey(sender)).setMeta({ chainId: CHAIN_ID, senderAccount: sender,}).addSigner(accountKey(sender)).setMeta({ chainId: CHAIN_ID, senderAccount: sender,})The
addCandidatefunction also implements a preflight request. The preflight request allows you to test a transaction without sending it. The response to the preflight request contains information about the expected success of the transaction and the how much gas the transaction requires. If the transaction would fail or the gas fee is higher than you would like, you can choose not to send the transaction.const preflightResponse = await client.preflight(signedTx); if (preflightResponse.result.status === 'failure') { throw preflightResponse.result.error;}const preflightResponse = await client.preflight(signedTx); if (preflightResponse.result.status === 'failure') { throw preflightResponse.result.error;}The remainder of the
addCandidatefunction deals with sending the transaction and processing the response. -
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 adds a candidate to the database.
-
Click Accounts in the Chainweaver navigation panel, then copy the account name for your administrative account.
-
Open
http://localhost:5173in your browser, then click Set Account. -
Paste your administrative account, then click Save.
-
Click Add candidate, type the candidate information, then click Save.
Type candidate information using the following format:
{ "key": "1", "name": "Your name" }{ "key": "1", "name": "Your name" } -
Click Sign All.
After signing the request, a loading indicator is displayed on the website while the transaction is in progress. As soon as the transaction completes successfully, the candidate you nominated is added to the list.
Next steps
In this tutorial, you learned how to:
- Upgrade the smart contract for your election website.
- Include a
candidatesdatabase table and functions for listing and adding candidates to the table. - Connect the frontend of the election website to the local development network as a backend.
In the next tutorial, you'll upgrade the election module to enable people to
cast a vote on a candidate with their Kadena account.
To see the code for the activity you completed in this tutorial and get the
starter code for the next tutorial, check out the 08-voting branch from the
election-dapp repository by running the following command in your terminal
shell:
git checkout 08-votinggit checkout 08-voting