Building a voting dApp
One of the best ways to learn a new technology is to get your hands dirty and build applications with it. In this tutorial we'll learn to use Kadena's smart contract language, Pact, to build a minimal voting application that runs on the Chainweb blockchain.
Along the way, we'll learn how to write and test smart contracts in Pact, the basic architecture of dApps on Chainweb, and about major concepts like gas stations and deploying contracts. We'll also get acquainted with several useful tools in the Kadena ecosystem, including the Pact local test server and JavaScript libraries you can use to interact with nodes running Pact.
Voting on the Blockchain
Elections are a necessary part of democracies and democratic organizations. The voting systems used to administer elections must ensure a fair process and trustworthy result — easier said than done! Election security is a deep, fascinating topic, especially when it comes to online voting.
Blockchain technologies are well-suited to help secure online elections. A public blockchain gives participants a single view of all transactions, which makes it easy to verify votes without trusting a central election authority to tally and report the results.
Blockchain technologies don't solve all election security issues, but they're a strong foundation and researchers have proposed fully-secure online voting systems based on them. They've also seen success in the real world: Thailand's Democrat party held an election in which more than 120,000 registered Democrats voted via blockchain.
What We're Building
We'll build a tiny voting dApp prototype that lets anyone with a Kadena wallet address vote for a candidate from a selection of candidates. Each voter (i.e. address) can vote once. Some Kadena accounts are chosen as "election officials", and the smart contract grants them special privileges to select the candidates. Election officials can add new candidates at any time (but they can't remove candidates or adjust their votes).
Once the app is deployed, the election has begun! The frontend for our dApp will help users submit their votes and will display the total votes received by each candidate.
Setup
Requirements
Create Project Structure
Let's start by creating a basic project structure. Open your terminal and run the commands below:
mkdir election-dapp && cd election-dapp
mkdir pact
mkdir frontend
mkdir election-dapp && cd election-dapp
mkdir pact
mkdir frontend
We've got the election-dapp
directory and two additional sub-directories:
pact
, which holds the smart contractsfrontend
, which holds the frontend part of our application
Implementing the Voting Smart Contract
A typical developer workflow looks like this:
- Write contract code in
.pact
files - Write tests in
.repl
files - Execute your tests in the REPL
- Deploy to local pact server
- Deploy to Testnet
In this section we will focus on steps 1 to 3. Later, we'll deploy our smart contract to a local Pact server and to Testnet (the test version of Chainweb).
In your project directory, let's create two files:
pact/election.pact
, which will hold the source code for our smart contractpact/election.repl
, which will hold our tests
We also have to import some dependencies to our project but first let's provide
some context to better understand why we need them. In the introduction we
explained that our voting smart contract allows anyone with a wallet address to
vote for a candidate. Kadena uses an account model so creating a wallet means
creating an account for the native coin, KDA, which is a smart contract deployed
on Kadena blockchain. The name of this contract is intuitively coin
.
The coin
contract itself has two additional dependencies:
fungible-v2
, an interface that each fungible token deployed on Kadena should implementfungible-xchain-v1
, an interface that provides standard capability for cross-chain transfers.
To be able to properly test our voting contract we will need to invoke functions
defined in the coin
contract so we have to include it in our project together
with its dependencies, the fungible-v2
and fungible-xchain-v1
interfaces.
Please find the latest versions of those modules here:
Make sure to add these files to your project in the pact/root/
directory. You
should have 3 new files: coin-v5.pact
, fungible-v2.pact
,
fungible-xchain-v1.pact
.
Before we begin writing code, let's recap the features of our voting contract:
- Voters can record 1 vote for a candidate of their choice from the list of options. In response to voting, we'll return confirmation of their vote by returning their vote back to them.
- Election administrators can add candidates. The "add candidate" functionality should be guarded to only allow access to keys belonging to the election administrator accounts.
Election module
We're going to start by creating a Pact module called election
and define a
keyset named election-admin-keyset
which is used to guard certain features of
the module.
Let's copy the following code in the election.pact
file:
;; election.pact
(namespace 'free)
;; Define a keyset with name `election-admin-keyset`.
;; Keysets cannot be created in code, thus we read them in from the load message data.
(define-keyset "free.election-admin-keyset" (read-keyset 'election-admin-keyset))
;; Define `election` module
(module election GOVERNANCE
"Election demo module"
(defcap GOVERNANCE ()
"Module governance capability that only allows the admin to update this module"
;; Check if the tx was signed with the provided keyset, fail if not
(enforce-keyset "free.election-admin-keyset")
)
)
;; election.pact
(namespace 'free)
;; Define a keyset with name `election-admin-keyset`.
;; Keysets cannot be created in code, thus we read them in from the load message data.
(define-keyset "free.election-admin-keyset" (read-keyset 'election-admin-keyset))
;; Define `election` module
(module election GOVERNANCE
"Election demo module"
(defcap GOVERNANCE ()
"Module governance capability that only allows the admin to update this module"
;; Check if the tx was signed with the provided keyset, fail if not
(enforce-keyset "free.election-admin-keyset")
)
)
The GOVERNANCE
keyword on the module definition line is the module governance
capability and it references the capability defined right below using the
defcap
construct. It's purpose is to restrict access to the module upgrade and
administration operations, for example later on we'll add an insert-candidate
function that only administrators should be able to call and we'll use the
GOVERNANCE capability to guard it. The implementation can be as simple as in our
example, enforcing a keyset or more complex like tallying a stakeholder vote on
an upgrade hash.
Capabilities
Capabilities offer a system to manage user rights in an explicit way, i.e. allow a user to perform some sensitive task if the required capability has been successfully acquired. If not, the transaction will fail.
Our module already defines one capability, the module governance capability. In
addition to that, we're going to define the ACCOUNT-OWNER
and the VOTED
capabilities. The VOTED
capability is used to emit an event when a vote has
been made. The ACCOUNT-OWNER
capability validates the ownership of the KDA
account that's used to identify a user. This might not be clear at first but
let's look at the code:
;; election.pact
;; Import `coin` module while only making the `details` function available
;; in the `election` module body
(use coin [ details ])
(defcap ACCOUNT-OWNER (account:string)
"Make sure the requester owns the KDA account"
;; Get the guard of the given KDA account using coin.details function
;; and execute it using `enforce-guard`
(enforce-guard (at 'guard (coin.details account)))
)
(defcap VOTED (candidateId:string)
"Emit an event that indicates a vote has been made for the provided candidate"
@event
true
)
;; election.pact
;; Import `coin` module while only making the `details` function available
;; in the `election` module body
(use coin [ details ])
(defcap ACCOUNT-OWNER (account:string)
"Make sure the requester owns the KDA account"
;; Get the guard of the given KDA account using coin.details function
;; and execute it using `enforce-guard`
(enforce-guard (at 'guard (coin.details account)))
)
(defcap VOTED (candidateId:string)
"Emit an event that indicates a vote has been made for the provided candidate"
@event
true
)
The user submitting the vote is identified by the account
parameter that we
will pass to the vote
function that we'll implement later on. This parameter
can take any value which is why we need to make sure the value provided is the
correct one, in our case it should be the KDA account controlled by the tx
initiator. Every KDA account has a guard which controls access to it and we're
using the coin.details
function that returns an object of type
fungible-v2.account-details
to retrieve this guard for the provided account.
Finally we execute the guard using the built-in Pact function enforce-guard
.
Don't forget to add the snippet above in the election
module body.
Tables and data storage
So far we've defined a module and implemented some capabilities. Now we're going to talk about storing data. We have to store who the candidates are so you can vote for them and as well as who voted already so we can prevent double-voting.
Pact smart contracts store data in tables and each table has its own schema. For
our voting contract we need 2 tables: candidates
and votes
.
;; election.pact
;; Define the `candidates-schema` schema
(defschema candidates-schema
"Candidates table schema"
;; Candidates table has 2 columns, `name` of type string
;; and `votes` which is an `integer`
name:string
votes:integer)
;; Define the `votes-schema` schema
(defschema votes-schema
"Votes table schema"
;; Votes table has one column, `cid` - Candidate id of type string
cid:string
)
;; Define the `votes` table that's using the `votes-schema`
(deftable votes:{votes-schema})
;; Define the `candidates` table that's using the `candidates-schema`
(deftable candidates:{candidates-schema})
;; election.pact
;; Define the `candidates-schema` schema
(defschema candidates-schema
"Candidates table schema"
;; Candidates table has 2 columns, `name` of type string
;; and `votes` which is an `integer`
name:string
votes:integer)
;; Define the `votes-schema` schema
(defschema votes-schema
"Votes table schema"
;; Votes table has one column, `cid` - Candidate id of type string
cid:string
)
;; Define the `votes` table that's using the `votes-schema`
(deftable votes:{votes-schema})
;; Define the `candidates` table that's using the `candidates-schema`
(deftable candidates:{candidates-schema})
To summarize, we created a table to store candidates and their associated vote counts and one for storing what accounts have already voted to prevent double-voting.
Functionality
We've defined our data storage so now we can add functions to read and write data, i.e. candidates and votes. One of the core features of our voting contract is to allow users to vote for a candidate while preventing double-voting so let's implement it:
;; election.pact
(defun user-voted:bool (account:string)
"Check if a user already voted"
;; Read from the votes table using `account` param value as key
;; with-default-read allows us to set default values for the table columns
;; that are returned if the row does not exist.
(with-default-read votes account
;; In this case we're setting the `cid` column default value to an empty string
{ "cid": "" }
{ "cid":= cid }
;; Check if `cid` is an empty string or not, return true if not,
;; i.e. user already voted and false otherwise,
;; meaning the user did not vote yet
(> (length cid) 0))
)
(defun candidate-exists:bool (cid:string)
"Check if a candidate exists"
;; Using a similar approach as in `user-voted` function,
;; in this case to check if a candidate exists
(with-default-read candidates cid
{ "name": "" }
{ "name" := name }
(> (length name) 0))
)
(defun vote-protected (account:string candidateId:string)
"Safe vote"
;; Check that the ACCOUNT-OWNER capability has already been granted, fail if not
(require-capability (ACCOUNT-OWNER account))
;; Read the current number of votes the candidate has
(with-read candidates candidateId { "votes" := votesCount }
;; Increment the number of votes by 1
(update candidates candidateId { "votes": (+ votesCount 1) })
;; Record the vote in the `votes` table (prevent double-voting)
(insert votes account { "cid": candidateId })
;; Emit an event that can be used by the frontend component to update the number of
;; votes displayed for a candidate
(emit-event (VOTED candidateId))
)
)
(defun vote (account:string cid:string)
"Vote for a candidate"
;; Prevent double-voting by checking if the user already voted through `user-voted` function
;; and `enforce` the returned value is `false`
(let ((double-vote (user-voted account)))
(enforce (= double-vote false) "Multiple voting not allowed"))
;; Prevent voting for a candidate that doesn't exist through `candidate-exists`
;; function and `enforce` the returned value is `true`
(let ((exists (candidate-exists cid)))
(enforce (= exists true) "Candidate doesn't exist"))
;; Try to acquire the `ACCOUNT-OWNER` capability which checks
;; that the transaction owner is also the owner of the KDA account provided as parameter to our `vote` function.
(with-capability (ACCOUNT-OWNER account)
;; While the `ACCOUNT-OWNER` capability is in scope we are calling `vote-protected` which is the function that updates the database
(vote-protected account cid))
(format "Voted for candidate {}!" [cid])
)
;; election.pact
(defun user-voted:bool (account:string)
"Check if a user already voted"
;; Read from the votes table using `account` param value as key
;; with-default-read allows us to set default values for the table columns
;; that are returned if the row does not exist.
(with-default-read votes account
;; In this case we're setting the `cid` column default value to an empty string
{ "cid": "" }
{ "cid":= cid }
;; Check if `cid` is an empty string or not, return true if not,
;; i.e. user already voted and false otherwise,
;; meaning the user did not vote yet
(> (length cid) 0))
)
(defun candidate-exists:bool (cid:string)
"Check if a candidate exists"
;; Using a similar approach as in `user-voted` function,
;; in this case to check if a candidate exists
(with-default-read candidates cid
{ "name": "" }
{ "name" := name }
(> (length name) 0))
)
(defun vote-protected (account:string candidateId:string)
"Safe vote"
;; Check that the ACCOUNT-OWNER capability has already been granted, fail if not
(require-capability (ACCOUNT-OWNER account))
;; Read the current number of votes the candidate has
(with-read candidates candidateId { "votes" := votesCount }
;; Increment the number of votes by 1
(update candidates candidateId { "votes": (+ votesCount 1) })
;; Record the vote in the `votes` table (prevent double-voting)
(insert votes account { "cid": candidateId })
;; Emit an event that can be used by the frontend component to update the number of
;; votes displayed for a candidate
(emit-event (VOTED candidateId))
)
)
(defun vote (account:string cid:string)
"Vote for a candidate"
;; Prevent double-voting by checking if the user already voted through `user-voted` function
;; and `enforce` the returned value is `false`
(let ((double-vote (user-voted account)))
(enforce (= double-vote false) "Multiple voting not allowed"))
;; Prevent voting for a candidate that doesn't exist through `candidate-exists`
;; function and `enforce` the returned value is `true`
(let ((exists (candidate-exists cid)))
(enforce (= exists true) "Candidate doesn't exist"))
;; Try to acquire the `ACCOUNT-OWNER` capability which checks
;; that the transaction owner is also the owner of the KDA account provided as parameter to our `vote` function.
(with-capability (ACCOUNT-OWNER account)
;; While the `ACCOUNT-OWNER` capability is in scope we are calling `vote-protected` which is the function that updates the database
(vote-protected account cid))
(format "Voted for candidate {}!" [cid])
)
A quick recap: we implemented a vote
function that allows to vote for a
candidate while preventing double-voting, voting for a candidate that doesn't
exist or voting with an account that the user doesn't own.
Now that we can vote, we also need a function to read the number of votes a candidate received:
(defun get-votes:integer (cid:string)
"Get the votes count by cid"
;; Read the row using cid as key and select only the `votes` column
(at 'votes (read candidates cid ['votes]))
)
(defun get-votes:integer (cid:string)
"Get the votes count by cid"
;; Read the row using cid as key and select only the `votes` column
(at 'votes (read candidates cid ['votes]))
)
Last thing on the list is adding candidates:
(defun insert-candidate (candidate)
"Insert a new candidate, admin operation"
;; Try to acquire the GOVERNANCE capability
(with-capability (GOVERNANCE)
;; While GOVERNANCE capability is in scope, insert the candidate
(let ((name (at 'name candidate)))
;; The key has to be unique, otherwise this operation will fail
(insert candidates (at 'key candidate) { "name": (at 'name candidate), "votes": 0 })))
)
(defun insert-candidates (candidates:list)
"Insert a list of candidates"
;; Using the above defined `insert-candidate` to bulk-insert a list of candidates
(map (insert-candidate) candidates)
)
(defun insert-candidate (candidate)
"Insert a new candidate, admin operation"
;; Try to acquire the GOVERNANCE capability
(with-capability (GOVERNANCE)
;; While GOVERNANCE capability is in scope, insert the candidate
(let ((name (at 'name candidate)))
;; The key has to be unique, otherwise this operation will fail
(insert candidates (at 'key candidate) { "name": (at 'name candidate), "votes": 0 })))
)
(defun insert-candidates (candidates:list)
"Insert a list of candidates"
;; Using the above defined `insert-candidate` to bulk-insert a list of candidates
(map (insert-candidate) candidates)
)
Inserting a new candidate is an "admin-only" operation and we reused the already
defined GOVERNANCE
capability to guard it.
We have now essentially completed our module. All the required functionality is implemented.
When a module is deployed, the tables that it defines need to be created. This
is done using the create-table
function. Insert the snippet below after the
module's closing parenthesis:
;; election.pact
;; Read the `upgrade` key from transaction data
(if (read-msg "upgrade")
;; If its value is true, it means we're upgrading the module
["upgrade"]
;; Otherwise, the transaction is deploying the module and we need to create the tables
[
(create-table candidates)
(create-table votes)
]
)
;; election.pact
;; Read the `upgrade` key from transaction data
(if (read-msg "upgrade")
;; If its value is true, it means we're upgrading the module
["upgrade"]
;; Otherwise, the transaction is deploying the module and we need to create the tables
[
(create-table candidates)
(create-table votes)
]
)
You can find the complete source code of the election.pact
contract in the
voting dApp repository.
It's time to summarize what we've learned so far:
- we can use Pact capabilities to protect certain features of our smart contract
- we should design our tables in such a way that we can retrieve the information using a single row query
- we can validate the owner of an account by executing its guard
These are general concepts to keep in mind when developing Pact smart contracts.
Namespaces
Each module or interface needs to be part of a namespace. To set the namespace
of a module we have to use the namespace
function. Ensure the following line
is at the beginning of your .pact
files:
(namespace 'free)
(namespace 'free)
Within the same namespace, each module name needs to be unique, similar requirement for defined keysets.
Access a module's function using the fully qualified name
{namespace}.{module-name}.{function-name}
, e.g. free.election.vote
. See the
Pact documentation to
read more about namespaces.
The free
namespace is available to use on both mainnet
and testnet
.
Testing the contract
We wrote quite a bit of code but at this point we don't know if it's working correctly. A critical step in smart-contract development process is writing a proper set of tests which is what we're going to focus on now.
We're going to start by setting up the environment data that we need for our
tests, load the required modules, i.e. coin
module and of our election
module and create some KDA accounts that we will use to vote later on.
Create the election.repl
file and copy the snippet below:
;; election.repl
;; begin-tx and commit-tx simulate a transaction
(begin-tx "Load modules")
;; set transaction JSON data
(env-data {
;; Here we set the required keysets.
;; Note:
;; - in a real transaction, `admin-key` would be a public key
;; - "keys-all" is a built-in predicate that specifies all keys are needed to sign a tx,
;; in this case we only set one key
'election-admin-keyset: { "keys": ["admin-key"], "pred": "keys-all" },
'alice-keyset: { "keys": ["alice-key"], "pred": "keys-all" },
'bob-keyset: { "keys": ["bob-key"], "pred": "keys-all" },
'namespace-keyset: { "keys": [ ], "pred": "keys-all" },
;; Upgrade key is set to false because we are deploying the modules
'upgrade: false
})
;; All Pact modules must exist within a namespace on Chainweb, except for basic contracts provided by Kadena.
;; There are two namespaces available for anyone to use on Chainweb: the 'free' namespace and the 'user'
;; namespace. Our contract uses the "free" namespace, so we need to make sure it exists in our REPL
;; environment.
;; Defining a namespace requires that we provide two keysets. The first keyset indicates the user that must
;; have signed any transaction that deploys code to the given namespace. The second keyset is the namespace
;; admin's keyset, and it indicates that the admin must sign the transaction that creates the new namespace.
;; For testing purposes we will use the same mock namespace keyset for both.
(define-namespace "free" (read-keyset "namespace-keyset") (read-keyset "namespace-keyset"))
;; load fungible-v2 interface
(load "root/fungible-v2.pact")
;; load fungible-xchain-v1 interace
(load "root/fungible-xchain-v1.pact")
;; load coin module
(load "root/coin-v5.pact")
;; create coin module tables
(create-table coin.coin-table)
(create-table coin.allocation-table)
;; load election module
(load "election.pact")
;; commit the transaction
(commit-tx)
(begin-tx "Create KDA accounts")
;; create "alice" KDA account
(coin.create-account "alice" (read-keyset "alice-keyset"))
;; create "bob" KDA account
(coin.create-account "bob" (read-keyset "bob-keyset"))
(commit-tx)
;; election.repl
;; begin-tx and commit-tx simulate a transaction
(begin-tx "Load modules")
;; set transaction JSON data
(env-data {
;; Here we set the required keysets.
;; Note:
;; - in a real transaction, `admin-key` would be a public key
;; - "keys-all" is a built-in predicate that specifies all keys are needed to sign a tx,
;; in this case we only set one key
'election-admin-keyset: { "keys": ["admin-key"], "pred": "keys-all" },
'alice-keyset: { "keys": ["alice-key"], "pred": "keys-all" },
'bob-keyset: { "keys": ["bob-key"], "pred": "keys-all" },
'namespace-keyset: { "keys": [ ], "pred": "keys-all" },
;; Upgrade key is set to false because we are deploying the modules
'upgrade: false
})
;; All Pact modules must exist within a namespace on Chainweb, except for basic contracts provided by Kadena.
;; There are two namespaces available for anyone to use on Chainweb: the 'free' namespace and the 'user'
;; namespace. Our contract uses the "free" namespace, so we need to make sure it exists in our REPL
;; environment.
;; Defining a namespace requires that we provide two keysets. The first keyset indicates the user that must
;; have signed any transaction that deploys code to the given namespace. The second keyset is the namespace
;; admin's keyset, and it indicates that the admin must sign the transaction that creates the new namespace.
;; For testing purposes we will use the same mock namespace keyset for both.
(define-namespace "free" (read-keyset "namespace-keyset") (read-keyset "namespace-keyset"))
;; load fungible-v2 interface
(load "root/fungible-v2.pact")
;; load fungible-xchain-v1 interace
(load "root/fungible-xchain-v1.pact")
;; load coin module
(load "root/coin-v5.pact")
;; create coin module tables
(create-table coin.coin-table)
(create-table coin.allocation-table)
;; load election module
(load "election.pact")
;; commit the transaction
(commit-tx)
(begin-tx "Create KDA accounts")
;; create "alice" KDA account
(coin.create-account "alice" (read-keyset "alice-keyset"))
;; create "bob" KDA account
(coin.create-account "bob" (read-keyset "bob-keyset"))
(commit-tx)
Now that this initial setup is done, we can go on and write some tests. Notice that we did not add any candidates just yet so any attempt to vote at this point should fail. Let's try it:
;; election.repl
(begin-tx "Vote for non-existing candidate")
;; set the key signing this transaction, `alice-key`
;; setting `caps` as an empty array translates into `unrestricted mode`, meaning our keyset
;; can be used to sign anything, it's not restricted to a specific set of capabilities
(env-sigs [{ "key": "alice-key", "caps": []}])
;; this test passes because the election.vote call fails
(expect-failure "Can't vote for a non-existing candidate" (free.election.vote "alice" "5"))
(commit-tx)
;; election.repl
(begin-tx "Vote for non-existing candidate")
;; set the key signing this transaction, `alice-key`
;; setting `caps` as an empty array translates into `unrestricted mode`, meaning our keyset
;; can be used to sign anything, it's not restricted to a specific set of capabilities
(env-sigs [{ "key": "alice-key", "caps": []}])
;; this test passes because the election.vote call fails
(expect-failure "Can't vote for a non-existing candidate" (free.election.vote "alice" "5"))
(commit-tx)
In the snippet above we've learned that we can use expect-failure
to test that
an expression will fail and that we can configure the keys and capabilities
signing a transaction using env-sigs
.
:::note REPL-Only Functions expect-failure
and env-sigs
are two of the many
REPL-only functions that we can use in .repl
files to test Pact
smart-contracts by simulating blockchain environment. You can check the
complete list of REPL-only functions
in the Pact official documentation. :::
Next we're going to add some candidates and check if their number of votes is correctly initialized.
;; election.repl
(begin-tx "Add candidates")
(use free.election)
;; Need to provide the key that is part of the election-admin-keyset
(env-sigs [{ "key": "admin-key", "caps": []}])
;; Call `insert-candidates` to add 3 candidates
(free.election.insert-candidates [{ "key": "1", "name": "Candidate A" } { "key": "2", "name": "Candidate B" } { "key": "3", "name": "Candidate B" }])
;; test if votes count for candidate "1" is initialized with 0
(expect "votes for Candidate A initialized" (get-votes "1") 0)
;; test if votes count for candidate "2" is initialized with 0
(expect "votes for Candidate B initialized" (get-votes "2") 0)
;; test if votes count for candidate "3" is initialized with 0
(expect "votes for Candidate C initialized" (get-votes "3") 0)
(commit-tx)
;; election.repl
(begin-tx "Add candidates")
(use free.election)
;; Need to provide the key that is part of the election-admin-keyset
(env-sigs [{ "key": "admin-key", "caps": []}])
;; Call `insert-candidates` to add 3 candidates
(free.election.insert-candidates [{ "key": "1", "name": "Candidate A" } { "key": "2", "name": "Candidate B" } { "key": "3", "name": "Candidate B" }])
;; test if votes count for candidate "1" is initialized with 0
(expect "votes for Candidate A initialized" (get-votes "1") 0)
;; test if votes count for candidate "2" is initialized with 0
(expect "votes for Candidate B initialized" (get-votes "2") 0)
;; test if votes count for candidate "3" is initialized with 0
(expect "votes for Candidate C initialized" (get-votes "3") 0)
(commit-tx)
We can use expect
function to test that any 2 expressions value is equal, in
this case we checked if get-votes
returns 0 for each candidate.
Moving on, we want to validate that votes are correctly recorded, the VOTED
event is emitted and double-voting is not allowed.
;; election.repl
(begin-tx)
(use free.election)
;; we set the key signing this tx and the capabilities that can be signed
;; coin.GAS is a capability that allows gas payments, we will talk more about gas and gas stations in the
;; next section
;; election.ACCOUNT-OWNER is the capability we implemented that validates the owner of the KDA account
(env-sigs [{ "key": "alice-key", "caps": [(coin.GAS), (free.election.ACCOUNT-OWNER "alice")]}])
;; test if votes count for candidate "1" is correctly increased by 1
;; 1. Retrieve the number of votes
(let ((count (get-votes "1")))
;; 2. Vote
(vote "alice" "1")
;; 3. Check if the vote was correctly recorded
(expect "votes count is increased by 1" (get-votes "1") (+ count 1)))
;; Test if the `VOTED` event with parameter "1" was emitted in this transaction
(expect "voted event"
[ { "name": "free.election.VOTED", "params": ["1"], "module-hash": (at 'hash (describe-module "free.election"))}]
(env-events true))
;; execute the same test using a different account
(env-sigs [{ "key": "bob-key", "caps": [(coin.GAS), (free.election.ACCOUNT-OWNER "bob")]}])
;; test if votes count for candidate "2" is correctly increased by 1
(let ((count (get-votes "2")))
(vote "bob" "2")
(expect "votes count is increased by 1" (get-votes "2") (+ count 1)))
(expect "voted event"
[ { "name": "free.election.VOTED",
"params": ["2"],
"module-hash": (at 'hash (describe-module "free.election"))
}
]
(env-events true))
;; test that bob's attempt to vote twice fails
(expect-failure "Double voting not allowed" (vote "bob" "1"))
(commit-tx)
;; election.repl
(begin-tx)
(use free.election)
;; we set the key signing this tx and the capabilities that can be signed
;; coin.GAS is a capability that allows gas payments, we will talk more about gas and gas stations in the
;; next section
;; election.ACCOUNT-OWNER is the capability we implemented that validates the owner of the KDA account
(env-sigs [{ "key": "alice-key", "caps": [(coin.GAS), (free.election.ACCOUNT-OWNER "alice")]}])
;; test if votes count for candidate "1" is correctly increased by 1
;; 1. Retrieve the number of votes
(let ((count (get-votes "1")))
;; 2. Vote
(vote "alice" "1")
;; 3. Check if the vote was correctly recorded
(expect "votes count is increased by 1" (get-votes "1") (+ count 1)))
;; Test if the `VOTED` event with parameter "1" was emitted in this transaction
(expect "voted event"
[ { "name": "free.election.VOTED", "params": ["1"], "module-hash": (at 'hash (describe-module "free.election"))}]
(env-events true))
;; execute the same test using a different account
(env-sigs [{ "key": "bob-key", "caps": [(coin.GAS), (free.election.ACCOUNT-OWNER "bob")]}])
;; test if votes count for candidate "2" is correctly increased by 1
(let ((count (get-votes "2")))
(vote "bob" "2")
(expect "votes count is increased by 1" (get-votes "2") (+ count 1)))
(expect "voted event"
[ { "name": "free.election.VOTED",
"params": ["2"],
"module-hash": (at 'hash (describe-module "free.election"))
}
]
(env-events true))
;; test that bob's attempt to vote twice fails
(expect-failure "Double voting not allowed" (vote "bob" "1"))
(commit-tx)
Notice the let
construct that we used above, it is helpful when you need to
bind some variables to be in the same scope as other logic that uses them. In
our case we first loaded the number of votes and binded the result to count
variable which we compared with the new count after submitting a vote. Feel free
to read more about let
and let*
in Pact
official documentation.
The only thing left is to run these tests and confirm everything is working:
$ pact
pact> (load "election.repl")
(load "election.repl" true)
Let's recap what we've learned in this section:
- we can test Pact smart-contracts using
.repl
scripts that simulate blockchain environment through a set of REPL-only functions - before writing tests we need to make sure all required modules are loaded as well as KDA accounts are created if we need them
- we can test functions returned values, emitted events, failure scenarios (and much more that we couldn't cover)
Implementing the Gas Station
A unique feature of Kadena is the ability to allow gas to be paid by a different entity than the one who initiated the transaction. This entity is what we call a gas station.
Let's take a look at the gas-payer-v1
interface defining a capability and a
function:
(interface gas-payer-v1
(defcap GAS_PAYER:bool
( user:string
limit:integer
price:decimal
)
@doc
" Provide a capability indicating that declaring module supports \
\ gas payment for USER for gas LIMIT and PRICE. Functionality \
\ should require capability (coin.FUND_TX), and should validate \
\ the spend of (limit * price), possibly updating some database \
\ entry. \
\ Should compose capability required for 'create-gas-payer-guard'."
@model
[ (property (user != ""))
(property (limit > 0))
(property (price > 0.0))
]
)
(defun create-gas-payer-guard:guard ()
@doc
" Provide a guard suitable for controlling a coin account that can \
\ pay gas via GAS_PAYER mechanics. Generally this is accomplished \
\ by having GAS_PAYER compose an unparameterized, unmanaged capability \
\ that is required in this guard. Thus, if coin contract is able to \
\ successfully acquire GAS_PAYER, the composed 'anonymous' cap required \
\ here will be in scope, and gas buy will succeed."
)
)
(interface gas-payer-v1
(defcap GAS_PAYER:bool
( user:string
limit:integer
price:decimal
)
@doc
" Provide a capability indicating that declaring module supports \
\ gas payment for USER for gas LIMIT and PRICE. Functionality \
\ should require capability (coin.FUND_TX), and should validate \
\ the spend of (limit * price), possibly updating some database \
\ entry. \
\ Should compose capability required for 'create-gas-payer-guard'."
@model
[ (property (user != ""))
(property (limit > 0))
(property (price > 0.0))
]
)
(defun create-gas-payer-guard:guard ()
@doc
" Provide a guard suitable for controlling a coin account that can \
\ pay gas via GAS_PAYER mechanics. Generally this is accomplished \
\ by having GAS_PAYER compose an unparameterized, unmanaged capability \
\ that is required in this guard. Thus, if coin contract is able to \
\ successfully acquire GAS_PAYER, the composed 'anonymous' cap required \
\ here will be in scope, and gas buy will succeed."
)
)
Our module needs to implement all the functions and capabilities defined by the
gas-payer-v1
interface:
GAS_PAYER
capabilitycreate-gas-payer-guard
function
A gas station allows someone to debit from a coin account that they do not own, gas station account, to pay the gas fee for a transaction under certain conditions. How exactly that happens, let's see below.
Create a new file election-gas-station.pact
and paste the following snippet:
;; election-gas-station.pact
(module election-gas-station GOVERNANCE
(defcap GOVERNANCE ()
"Only admin can update the smart contract"
(enforce-keyset "free.election-admin-keyset"))
; Signal that the module implements the gas-payer-v1 interface
(implements gas-payer-v1)
; Import the coin module, we need it to create a KDA account that will be controlled
; by the gas station
(use coin)
)
;; election-gas-station.pact
(module election-gas-station GOVERNANCE
(defcap GOVERNANCE ()
"Only admin can update the smart contract"
(enforce-keyset "free.election-admin-keyset"))
; Signal that the module implements the gas-payer-v1 interface
(implements gas-payer-v1)
; Import the coin module, we need it to create a KDA account that will be controlled
; by the gas station
(use coin)
)
Next we will implement the gas-payer-v1
interface. We don't want to let users
abuse our gas station so we'll have to add a limit for the maximum gas price
we're willing to pay or make sure it can only be used to pay for transactions
that are calling the election
module. Let's get to it:
;; election-gas-station.pact
(defun chain-gas-price ()
"Return gas price from chain-data"
; chain-data is a built-in function that returns tx public metadata
; we are using it to retrieve the tx gas price
(at 'gas-price (chain-data)))
(defun enforce-below-or-at-gas-price:bool (gasPrice:decimal)
(enforce (<= (chain-gas-price) gasPrice)
(format "Gas Price must be smaller than or equal to {}" [gasPrice])))
(defcap GAS_PAYER:bool
( user:string
limit:integer
price:decimal
)
; There are 2 types of Pact transactions: exec and cont
; `cont` is used for multi-step pacts, `exec` is for regular transactions.
; In our case transaction has to be of type `exec`.
(enforce (= "exec" (at "tx-type" (read-msg))) "Inside an exec")
; A Pact transaction can have multiple function calls, but we only want to allow one
(enforce (= 1 (length (at "exec-code" (read-msg)))) "Tx of only one pact function")
; Gas station can only be used to pay for gas consumed by functions defined in `free-election` module
(enforce
; We take the first 15 characters and compare it with `(free.election`
; to make sure a function from our module is called.
; `free` is the namespace where our module will be deployed.
(= "(free.election." (take 15 (at 0 (at "exec-code" (read-msg)))))
"Only election module calls allowed")
;; Limit the gas price that the gas station can pay
(enforce-below-or-at-gas-price 0.000001)
; Import the `ALLOW_GAS` capability
(compose-capability (ALLOW_GAS))
)
;; election-gas-station.pact
(defun chain-gas-price ()
"Return gas price from chain-data"
; chain-data is a built-in function that returns tx public metadata
; we are using it to retrieve the tx gas price
(at 'gas-price (chain-data)))
(defun enforce-below-or-at-gas-price:bool (gasPrice:decimal)
(enforce (<= (chain-gas-price) gasPrice)
(format "Gas Price must be smaller than or equal to {}" [gasPrice])))
(defcap GAS_PAYER:bool
( user:string
limit:integer
price:decimal
)
; There are 2 types of Pact transactions: exec and cont
; `cont` is used for multi-step pacts, `exec` is for regular transactions.
; In our case transaction has to be of type `exec`.
(enforce (= "exec" (at "tx-type" (read-msg))) "Inside an exec")
; A Pact transaction can have multiple function calls, but we only want to allow one
(enforce (= 1 (length (at "exec-code" (read-msg)))) "Tx of only one pact function")
; Gas station can only be used to pay for gas consumed by functions defined in `free-election` module
(enforce
; We take the first 15 characters and compare it with `(free.election`
; to make sure a function from our module is called.
; `free` is the namespace where our module will be deployed.
(= "(free.election." (take 15 (at 0 (at "exec-code" (read-msg)))))
"Only election module calls allowed")
;; Limit the gas price that the gas station can pay
(enforce-below-or-at-gas-price 0.000001)
; Import the `ALLOW_GAS` capability
(compose-capability (ALLOW_GAS))
)
To recap, the GAS_PAYER
capability implementation performs a few checks and
composes the ALLOW_GAS
capability that we will define next. chain-gas-price
and enforce-below-or-at-gas-price
are helper functions to limit the gas price
that our gas station is willing to pay.
;; election-gas-station.pact
(defcap ALLOW_GAS () true)
(defun create-gas-payer-guard:guard ()
(create-user-guard (gas-payer-guard))
)
(defun gas-payer-guard ()
(require-capability (GAS))
(require-capability (ALLOW_GAS))
)
(defconst GAS_STATION "election-gas-station")
(defun init ()
(coin.create-account GAS_STATION (create-gas-payer-guard))
)
;; election-gas-station.pact
(defcap ALLOW_GAS () true)
(defun create-gas-payer-guard:guard ()
(create-user-guard (gas-payer-guard))
)
(defun gas-payer-guard ()
(require-capability (GAS))
(require-capability (ALLOW_GAS))
)
(defconst GAS_STATION "election-gas-station")
(defun init ()
(coin.create-account GAS_STATION (create-gas-payer-guard))
)
Then we can wrap it up and make sure the init
function is called when we're
deploying the module:
;; election-gas-station.pact
(if (read-msg 'upgrade)
["upgrade"]
[
(init)
]
)
;; election-gas-station.pact
(if (read-msg 'upgrade)
["upgrade"]
[
(init)
]
)
First we define the ALLOW_GAS
capability which is brought in scope by the
GAS_PAYER
capability through compose-capability
function.
:::note Composing capabilities allows for modular factoring of guard code, e.g. an "outer" capability could be composed out of multiple "inner" capabilities. Also composed capabilities are only in scope when their parent capability is granted. :::
Then we implement the gas-payer-guard
function which tests if GAS
(magic
capability defined in coin contract) and ALLOW_GAS
capabilities have been
granted which are needed to be able to pay for gas fees. By composing
ALLOW_GAS
in GAS_PAYER
we hide the implementation details of GAS_PAYER
that gas-payer-guard
function does not need to know about. This is then used
in create-gas-payer-guard
to create a special guard for the coin contract
account from where the gas fees are paid.
Last thing we need is to create an account where the funds will be stored which
is what happens in the init
function. As you can see, the guard of that
account is the guard returned by create-gas-payer-guard
, essentially allowing
access to the account as long as GAS
and ALLOW_GAS
capabilities have already
been granted.
To summarize, a gas station is a coin account with a special guard that's valid
if both GAS
and ALLOW_GAS
capabilities are granted. If you're wondering how
GAS_PAYER
is granted, the answer is
signature capabilities.
We will see how this works in the frontend section of this tutorial where we
interact with the smart contracts.
Deploying to Chainweb
To deploy our contracts to the real blockchain network, we'll need to pay for the transaction using gas fees (whether it's Testnet or Mainnet).
In this tutorial we are using Chainweaver wallet to create accounts and sign
transactions. Head over to Chainweaver and create an account on testnet
.
Next step is to fund your testnet
account using this
faucet. You will receive 20 Testnet KDA.
const { PactCommand } = require('@kadena/client');
const { createExp } = require('@kadena/pactjs');
const NETWORK_ID = 'testnet04';
const CHAIN_ID = '1';
const API_HOST = `https://api.testnet.chainweb.com/chainweb/0.0/${NETWORK_ID}/chain/${CHAIN_ID}/pact`;
listModules();
async function listModules() {
const pactCommand = new PactCommand();
const publicMeta = {
chainId: CHAIN_ID,
gasLimit: 6000,
gasPrice: 0.001,
ttl: 600,
};
pactCommand.code = createExp('list-modules');
pactCommand.setMeta(publicMeta, NETWORK_ID);
const response = await pactCommand.local(API_HOST);
console.log(response.result.data);
}
const { PactCommand } = require('@kadena/client');
const { createExp } = require('@kadena/pactjs');
const NETWORK_ID = 'testnet04';
const CHAIN_ID = '1';
const API_HOST = `https://api.testnet.chainweb.com/chainweb/0.0/${NETWORK_ID}/chain/${CHAIN_ID}/pact`;
listModules();
async function listModules() {
const pactCommand = new PactCommand();
const publicMeta = {
chainId: CHAIN_ID,
gasLimit: 6000,
gasPrice: 0.001,
ttl: 600,
};
pactCommand.code = createExp('list-modules');
pactCommand.setMeta(publicMeta, NETWORK_ID);
const response = await pactCommand.local(API_HOST);
console.log(response.result.data);
}
The snippets can also be found in the tutorial repository.
You can use the snippet below to deploy your contract to chain 1 on
testnet
. To do this, it's required to run Chainweaver locally to sign for the
transaction. Please see the
Chainweaver User Guide
for downloads and instructions.
Now we can install the dependencies and deploy the contract:
npm init -y
npm install @kadena/client @kadena/chainweb-node-client --save
npm init -y
npm install @kadena/client @kadena/chainweb-node-client --save
const { PactCommand, signWithChainweaver } = require('@kadena/client');
const fs = require('fs');
const NETWORK_ID = 'testnet04';
const CHAIN_ID = '1';
const API_HOST = `https://api.testnet.chainweb.com/chainweb/0.0/${NETWORK_ID}/chain/${CHAIN_ID}/pact`;
const CONTRACT_PATH = '../pact/election.pact';
const ACCOUNT_NAME = 'some-account-name';
const PUBLIC_KEY = 'some-public-key';
const pactCode = fs.readFileSync(CONTRACT_PATH, 'utf8');
deployContract(pactCode);
async function deployContract(pactCode) {
const publicMeta = {
ttl: 28000,
gasLimit: 100000,
chainId: CHAIN_ID,
gasPrice: 0.000001,
sender: ACCOUNT_NAME, // the account paying for gas
};
const pactCommand = new PactCommand()
.setMeta(publicMeta, NETWORK_ID)
.addCap('coin.GAS', PUBLIC_KEY)
.addData({
'election-admin-keyset': [PUBLIC_KEY],
upgrade: false,
});
pactCommand.code = pactCode;
const signedTransaction = await signWithChainweaver(pactCommand);
const response = await signedTransaction[0].send(API_HOST);
console.log(response);
}
const { PactCommand, signWithChainweaver } = require('@kadena/client');
const fs = require('fs');
const NETWORK_ID = 'testnet04';
const CHAIN_ID = '1';
const API_HOST = `https://api.testnet.chainweb.com/chainweb/0.0/${NETWORK_ID}/chain/${CHAIN_ID}/pact`;
const CONTRACT_PATH = '../pact/election.pact';
const ACCOUNT_NAME = 'some-account-name';
const PUBLIC_KEY = 'some-public-key';
const pactCode = fs.readFileSync(CONTRACT_PATH, 'utf8');
deployContract(pactCode);
async function deployContract(pactCode) {
const publicMeta = {
ttl: 28000,
gasLimit: 100000,
chainId: CHAIN_ID,
gasPrice: 0.000001,
sender: ACCOUNT_NAME, // the account paying for gas
};
const pactCommand = new PactCommand()
.setMeta(publicMeta, NETWORK_ID)
.addCap('coin.GAS', PUBLIC_KEY)
.addData({
'election-admin-keyset': [PUBLIC_KEY],
upgrade: false,
});
pactCommand.code = pactCode;
const signedTransaction = await signWithChainweaver(pactCommand);
const response = await signedTransaction[0].send(API_HOST);
console.log(response);
}
Make sure to replace ACCOUNT_NAME
and the PUBLIC_KEY
with the ones in your
local chainweaver. Also ensure the CHAIN_ID
matches the one having KDA
available. When everything goes according to plan, you should see something like
this:
$ node ./deploy-testnet.js
{ requestKeys: [ 'SufG_mxEf3GZcbgxtjLbfMPgBuShuk3MMK_T5uoB0QM' ] }
$ node ./deploy-testnet.js
{ requestKeys: [ 'SufG_mxEf3GZcbgxtjLbfMPgBuShuk3MMK_T5uoB0QM' ] }
Frontend
If you made it until here, congrats! We wrote, tested and deployed our smart contract. But we're still missing a key component: a UI for users to interact with our dApp, so let's get this done.
Start by adding the required libraries from Kadena.js as a dependency to your project either via a package manager or add it to your asset pipeline similar to any other JavaScript library.
npm init -y
npm install @kadena/client @kadena/chainweb-node-client --save
npm init -y
npm install @kadena/client @kadena/chainweb-node-client --save
Typescript
The Kadena.js team has created libraries that allow Javascript/Typescript users
to easily interact with the Kadena Blockchain. Also there's a commandline tool
pactjs-cli
that allows generation of types from pact contracts, which we're
going to make use of in this tutorial. Let's first add the required libraries to
your project.
npm install typescript @kadena/types --save-dev
npm install @kadena/pactjs-cli -g
npm install typescript @kadena/types --save-dev
npm install @kadena/pactjs-cli -g
Create a tsconfig.json
file in the root of the frontend folder and paste in
the following JSON:
{
"compilerOptions": {
"types": [".kadena/pactjs-generated"],
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
},
"lib": ["es2015"]
}
{
"compilerOptions": {
"types": [".kadena/pactjs-generated"],
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
},
"lib": ["es2015"]
}
From the root of the frontend folder, use the following commands to generate
types for our election
, election-gas-station
and coin
contracts.
Generating types for the coin
contract is necessary, because when paying for
gas we use the coin.GAS
capability from the coin contract.
npx pactjs contract-generate --file ../pact/election.pact;
npx pactjs contract-generate --file ../pact/election-gas-station.pact;
npx pactjs contract-generate --file ../pact/root/coin-v5.pact
npx pactjs contract-generate --file ../pact/election.pact;
npx pactjs contract-generate --file ../pact/election-gas-station.pact;
npx pactjs contract-generate --file ../pact/root/coin-v5.pact
The log shows what has happened. Inside the node_modules
directory, a new
package has been created: .kadena/pactjs-generated
. This package is extending
the @kadena/client types to give you type information. Make sure to add
"types": [".kadena/pactjs-generated"]
to your tsconfig.json.
Our implementation
There are a few key aspects concerning a frontend implementation of a blockchain application:
- reading data from smart contracts
- allowing users to sign and submit transactions
- notify users when various actions take place like a transaction being mined or a smart contract event was emitted
The code of this tutorial can be found in the frontend folder in the tutorial repository. For demonstration purposes the election smart contracts have been deployed to testnet chain 0 and testnet chain 1.
Read Data
For this demo application we would like to display the number of votes that each
candidate received. To do that we have to call the get-votes
function from our
election
module. Here's what that looks like:
// ./api.ts
import { Pact, signWithChainweaver } from '@kadena/client'
import { pollTransactions } from './utils'
const NETWORK_ID = 'testnet04'
const CHAIN_ID = '1'
const API_HOST = `https://api.testnet.chainweb.com/chainweb/0.0/${NETWORK_ID}/chain/${CHAIN_ID}/pact`
const accountKey = (account: string): string => account.split(':')[1]
/**
* Return the amount of votes a candidate has received
*
* @param candidateId - The candidate's id
* @return the number of votes
*/
export const getVotes = async (candidateId: string): Promise<number> => {
const transactionBuilder = Pact.modules['free.election']['get-votes'](candidateId)
const { result } = await transactionBuilder.local(API_HOST)
if (result.status === 'success') {
return result.data.valueOf() as number
} else {
console.log(result.error)
return 0
}
}
// ./api.ts
import { Pact, signWithChainweaver } from '@kadena/client'
import { pollTransactions } from './utils'
const NETWORK_ID = 'testnet04'
const CHAIN_ID = '1'
const API_HOST = `https://api.testnet.chainweb.com/chainweb/0.0/${NETWORK_ID}/chain/${CHAIN_ID}/pact`
const accountKey = (account: string): string => account.split(':')[1]
/**
* Return the amount of votes a candidate has received
*
* @param candidateId - The candidate's id
* @return the number of votes
*/
export const getVotes = async (candidateId: string): Promise<number> => {
const transactionBuilder = Pact.modules['free.election']['get-votes'](candidateId)
const { result } = await transactionBuilder.local(API_HOST)
if (result.status === 'success') {
return result.data.valueOf() as number
} else {
console.log(result.error)
return 0
}
}
We're sending a command to the /local
endpoint where the pactCode
attribute
is a call to our module function which returns the number of votes for the given
candidate.
:::note Remember to always use the fully qualified name, namespace.module.function. :::
Here's a screenshot from our demo app where we display the candidates and the number of votes received by each candidate:
Sign & Send Transaction
The next step is to allow users to vote for a candidate. When it comes to updating on-chain data, each dApp has to implement the following flow:
- Create transaction
- Sign transaction
- Send transaction
- Notify when transaction is mined
@kadena/client provides a couple of useful methods here:
signWithChainweaver
to interact with the Chainweaver signing API and send
on
the ICommandBuilder
to submit the signed transaction to the network.
In the snippet below we are constructing a transaction that calls the
free.election.vote
contract function to vote for a candidate.
// ./api.ts
/**
* Vote for a candidate and poll the transaction status afterwards
*
* @param account - The account that is voting
* @param candidateId - The candidateId that is being voted for
* @return
*/
export const vote = async (
account: string,
candidateId: string,
): Promise<void> => {
const transactionBuilder = Pact.modules['free.election']
.vote(account, candidateId)
.addCap('coin.GAS', accountKey(account))
.addCap('free.election.ACCOUNT-OWNER', accountKey(account), account)
.setMeta(
{
ttl: 28000,
gasLimit: 100000,
chainId: CHAIN_ID,
gasPrice: 0.000001,
sender: account,
},
NETWORK_ID,
);
const signedTransaction = await signWithChainweaver(transactionBuilder);
console.log(`Sending transaction: ${signedTransaction[0].code}`);
const response = await signedTransaction[0].send(API_HOST);
console.log('Send response: ', response);
const requestKey = response.requestKeys[0];
await pollTransactions([requestKey], API_HOST);
};
// ./api.ts
/**
* Vote for a candidate and poll the transaction status afterwards
*
* @param account - The account that is voting
* @param candidateId - The candidateId that is being voted for
* @return
*/
export const vote = async (
account: string,
candidateId: string,
): Promise<void> => {
const transactionBuilder = Pact.modules['free.election']
.vote(account, candidateId)
.addCap('coin.GAS', accountKey(account))
.addCap('free.election.ACCOUNT-OWNER', accountKey(account), account)
.setMeta(
{
ttl: 28000,
gasLimit: 100000,
chainId: CHAIN_ID,
gasPrice: 0.000001,
sender: account,
},
NETWORK_ID,
);
const signedTransaction = await signWithChainweaver(transactionBuilder);
console.log(`Sending transaction: ${signedTransaction[0].code}`);
const response = await signedTransaction[0].send(API_HOST);
console.log('Send response: ', response);
const requestKey = response.requestKeys[0];
await pollTransactions([requestKey], API_HOST);
};
Notice the addCap
function where we define the capabilities that the user's
keyset will have to sign. In this case we have two:
coin.GAS
-> enables the payment of gas feesfree.election.ACCOUNT-OWNER
-> checks if the user is the owner of the KDA account
Since this is a transaction that requires gas fees, we now set sender
(account
paying for gas) to the name of the KDA account of the user. If we would want to
utilize the gas station we deployed we would set the sender to the account owned
by our gas station election-gas-station
and use the
free.election-gas-station.GAS_PAYER
capability instead of coin.GAS
.
Lastly, to get the result of a transaction we are using the pollTransactions
helper method which can be found in the project repository.
To run the frontend dApp, go to the frontend folder and run:
npm run start
npm run start
Going back to the UI, we implemented this signing flow using a modal window where users have to enter their KDA account. Once the account is entered and the user hasn't voted yet the Vote Now button will become available. Clicking on the Vote Now button will automatically open the Chainweaver signing wizard.
Below is the first step of the Chainweaver request signing wizard:
Once the transaction is signed, our dApp modal will automatically submit it to the network.
The request key together with the transaction result are displayed in the browsers console output.
Note: Since mining is an external process, while waiting for our transaction to be included in the blockchain, the user should be able to keep using the application freely.
:::info As an extra excercise; modify the code to utilize the gasstation instead of having the user pay for gas fees. :::
Conclusion
It took a while but we are now at the end of this tutorial. Congratulations! You've managed to implement a complete dApp on Kadena blockchain and we hope you found this guide useful.
Stay tuned for more tutorials and we cannot wait to see what dApps YOU will build next!