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 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 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.pact
file in the code editor on your computer. -
Add the schema for the database table inside of the
election
module 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,
defschema
defines acandidate-schema
for a database table with two columns:name
of type string andvotes
of type integer. -
Create the table outside of the election module by adding the following lines of code at the end of the
./pact/election.pact
file, after the closing parenthesis ()
) of theelection
module definition:(if (read-msg "init-candidates") [(create-table candidates)] [])
(if (read-msg "init-candidates") [(create-table candidates)] [])
With this code, the
read-msg
function reads theinit-candidates
field from the transaction data. If you set this field totrue
in the data for your module deployment transaction, the statement between the first square brackets—(create-table candidates)
—is executed to create thecandidates
table based on its schema definition inside theelection
module.
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/pact
folder in the code editor on your computer. -
Create a new file named
election.repl
in thepact
folder. -
Set the
env-data
andenv-sigs
fields 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: true
is included in the environment data to ensure that the(create-table candidates)
command is executed when you load theelection
module into the Pact REPL. -
Define your principal namespace and the
admin-keyset
for the namespace using the principal namespace you used in yourelection.pact
file.(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 of the
election.pact
file, theelection
module is defined in your principal namespace and it is governed by theadmin-keyset
in 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
pact
command-line program running locally or using pact-cli from the Docker container.If
pact-cli
is installed locally, run the following command inside thepact
folder in current terminal shell:pact election.repl --trace
pact election.repl --trace
As before, if you don't have
pact
installed locally, you can load theelection.repl
file with the following command:(load "election.repl")
(load "election.repl")
If you are using the
pact-cli
in a browser, you can replace thepact election.repl --trace
command 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 successful
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 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.repl
file in the code editor on your computer. -
Add a transaction to test the current implementation of the
election.list-candidates
function:(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
pact
command-line program:pact election.repl --trace
pact election.repl --trace
If the current implementation of the
list-candidates
function 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 successful
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 successful
If 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 failed
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 failed
You can fix this issue by updating the return value of the
list-candidates
function in theelection-dapp/pact/election.pact
file. -
Open the
election-dapp/pact/election.pact
file in your code editor. -
Update the return value of the
list-candidates
function to select all of the rows of thecandidates
table, 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-db
function is like aSELECT * FROM table
statement in SQL, except that it fetches the value of thekey
column separately from the other column values.- The first argument for
fold-db
is 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
true
here. - 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-db
function 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
pact
command-line program:pact election.repl --trace
pact election.repl --trace
Because 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 successful
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 successful
Note that you shouldn't include a call to a function like
fold-db
in 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.repl
file in your code editor. -
Add a transaction to test that candidates have been added to the database using the
election.add-candidate
function:(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-candidate
function doesn't exist yet in theelection
module and you would see output similar to the following:election.repl:40:0:Error: Cannot resolve add-candidateLoad failed
election.repl:40:0:Error: Cannot resolve add-candidateLoad failed
However, from this code, you can see that the
add-candidate
function accepts a candidate object as an argument, and that the object is defined in JSON format.Notice that this object has the fields
key
andname
, while thecandidate-schema
you defined for thecandidates
table has two columnsname
andvotes
. Thevotes
column always has an initial value of0
when a new candidate is added, so you don't need to send a value for votes in the transaction.The
key
value 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.pact
file in your code editor. -
Define the
add-candidate
function inside theelection
module definition to receive acandidate
JSON object and call the built-ininsert
function:(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
insert
function:- The name of table you want to update.
In this case, the table is the
candidates
table. - The value for the key of the row to be inserted.
In this case, the value of the
key
field is extracted from thecandidate
object - The key-value object representing the row to be inserted into the table.
The keys correspond to the column names.
In this case, the
votes
column of the new value always gets a value0
and thename
column gets a value of"Candidate A"
,"Candidate B"
, or"Candidate C"
, as per your test cases.
Now that you've added the
add-candidate
function to theelection
module, you can test it using theelection.repl
file. - The name of table you want to update.
In this case, the table is the
-
Execute the transaction using the
pact
command-line program:pact election.repl --trace
pact election.repl --trace
You 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 successful
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 successful
The key of each row in a table must be unique. You can add a transaction to the
election.repl
file to test that you can't insert a row with a duplicate key. -
Add a transaction to the
election.repl
file to test that you can't insert a row with a duplicate key:(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 successful
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 successful
-
Verify that you only have three candidates in the table by adding the following assertion to the
election-dapp/pact/election.repl
file:(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
pact
command-line program:pact election.repl --trace
pact election.repl --trace
You 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 successful
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 successful
You've now seen how candidates can be stored in a database table and that the
list-candidates
function works as expected to retrieve information from that table. The next step is to restrict access to theadd-candidate
function, so that ony theelection
module owner can update thecandidates
database.
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.repl
file 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
pact
command-line program:pact election.repl --trace
pact election.repl --trace
You 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 failed
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 failed
-
Open the
election-dapp/pact/election.pact
file in your code editor. -
Update the
add-candidate
function 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-capability
function tries to bring theGOVERNANCE
in 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
pact
command-line program:pact election.repl --trace
pact election.repl --trace
You should see output similar to the following that verifies the
add-candidate
function is now guarded by theGOVERNANCE
capability: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 successful
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 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/snippets
folder in the terminal shell. -
Update 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-candidates
npm run deploy-module:devnet -- k:<your-public-key> upgrade init-candidates
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, you pass
upgrade
andinit-candidates
to 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 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:
{ 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
election
module.After you click View, the Module Explorer displays the
list-candidates
andadd-candidate
functions. If you click Open, you can view the module code in the editor pane and verify that theelection
module 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.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
listCandidates
function: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-ignore
comment 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/client
library). -
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 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
listCandidates
function:- Sets the chain identifier, gas limit, and network identifier before creating the transaction.
- Uses the
dirtyRead
method to preview the transaction result without sending a transaction to the blockchain. ThedirtyRead
method 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/frontend
directory is your current working directory. -
Install the frontend dependencies by running the following command:
npm install
npm install
-
Start the frontend application configured to use the
devnet
backend by running the following command:npm run start-devnet
npm run start-devnet
-
Open
http://localhost:5173
in 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
candidates
table before they can be displayed. To do that, you must first modify theaddCandidate
function in the frontend.
Add candidate
To modify the frontend to add candidates from the development network:
-
Open
election-dapp/frontend/src/repositories/candidate/DevnetCandidateRepository.ts
in your code editor. -
Review the
addCandidate
function.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-ignore
comment to enable the frontend function to call theadd-candidate
function in yourelection
module.The function takes the
candidate
object to insert data into thecandidates
database when the transaction is executed.Because the
add-candidate
function is guarded by theGOVERNANCE
capability that enforces theadmin-keyset
account, 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.repl
file. 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
senderAccount
field 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
addCandidate
function 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
addCandidate
function 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:5173
in 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
candidates
database 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-voting
git checkout 08-voting