Add a gas station
Traditional elections have minimal safeguards against fraud, corruption, mishandling of ballots, and intentional or unintentional disruptions. Even where voting is available by mail or online, elections can be costly, inefficient, and subject to human error.
By using blockchain technology, elections could be made more convenient, transparent, and reliable. For example:
- Every vote can be recorded as a public transaction that can't be altered.
- Voters can remain anonymous with votes linked to an encrypted digital fingerprint instead of government-issued identification.
- Election results can be independently verified by anyone.
However, there is one main drawback to using a blockchain to cast votes in an election. Because every vote is a public transaction that changes the state of the blockchain, every vote requires computational resources and incurs a processing fee—commonly referred to as a gas payment.
Paying for transaction processing is normal in the context of many business operations, but paying to vote is essentially undemocratic. To address this issue, Kadena introduced a transaction processing clearing house for paying fees called a gas station.
A gas station is an account that exists only to make transaction fee payments on behalf of other accounts and under specific conditions. For example, a government agency could apply a fraction of its budget for a traditional election to fund a gas station. The gas station could then pay the transaction fee for every voting transaction, allowing all citizens to vote for free.
For more information about the introduction of gas stations, see The First Crypto Gas Station is Now on Kadena’s Blockchain.
Before you begin
Before you start this tutorial, verify the following basic requirements:
- You have an internet connection and a web browser installed on your local computer.
- You have a code editor, such as Visual Studio Code, access to an interactive terminal shell, and are generally familiar with using command-line programs.
- You have cloned the voting-dapp repository to create your project directory as described in Prepare your workspace.
- You have the development network running in a Docker container as described in Start a local blockchain.
- You are connected to the development network using your local host IP address and port number 8080.
- You have created and funded an administrative account as described in Add an administrator account.
- You have created a principal namespace on the development network as described in Define a namespace.
- You have defined the keyset that controls your namespace using the administrative account as described in Define keysets.
- You have created an election Pact module and deployed it as described in Write a smart contract.
- You have updated and deployed the election smart contract on the development network as described in Nominate candidates and Add vote management.
Create a voter account
In the previous tutorial, you voted with your administrative account. The transaction was successful because the account had sufficient funds to pay the transaction fee. For this tutorial, you need to create a new voter account on the development network. Initially, you'll use the voter account to see that voting in the election application requires you to have funds in an account.
The steps for creating the voter account are similar to the steps you followed to create your administrative account.
To create a voter account:
-
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.
-
Click Keys in the Chainweaver navigation panel.
-
Click Generate Key to add a new public key to your list of public keys.
-
Click Add k: Account for the new public key to add a new account to the list of accounts you are watching in Chainweaver.
If you expand the new account, you'll see that no balance exists for the account on any chain and there's no information about the owner or keyset for the account.
-
Open the
election-dapp/snippets/create-account.ts
file in the code editor on your computer.You'll notice that this script is similar to the
./snippets/transfer-create.ts
script you used previously. The script imports functions from the Kadena client library to call thecreate-account
function of thecoin
contract to create a voter account. However, themain
function doesn't pass any funds or sign for theCOIN.TRANSFER
capability. -
Open the
election-dapp/snippets
folder in a terminal shell on your computer. -
Run the following command to create a new voter account.
npm run create-account:devnet -- k:<voter-public-key>
npm run create-account:devnet -- k:<voter-public-key>
Remember that
k:<voter-public-key>
is the default account name for the new voter account that you generated keys for. You can copy this account name from Chainweaver when viewing the account watch list.After a few seconds, you should see a status message:
{ status: 'success', data: 'Write succeeded' }
{ status: 'success', data: 'Write succeeded' }
You can also verify that the account was created by checking the account details using the Kadena client:
npm run coin-details:devnet -- k:<voter-public-key>
npm run coin-details:devnet -- k:<voter-public-key>
After running this command, you should see output similar to the following for the new voter account:
{ guard: { pred: 'keys-all', keys: [ 'bbccc99ec9eeed17d60159fbb88b09e30ec5e63226c34544e64e750ba424d35e' ] }, balance: 0, account: 'k:bbccc99ec9eeed17d60159fbb88b09e30ec5e63226c34544e64e750ba424d35e'}
{ guard: { pred: 'keys-all', keys: [ 'bbccc99ec9eeed17d60159fbb88b09e30ec5e63226c34544e64e750ba424d35e' ] }, balance: 0, account: 'k:bbccc99ec9eeed17d60159fbb88b09e30ec5e63226c34544e64e750ba424d35e'}
If you view the account in Chainweaver, you'll see similar information for the new account.
Attempt to cast a vote
To attempt to cast a vote with the voter account:
-
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 voter account name with the k: prefix exists on chain 1.
- Your voter account name has no KDA account balance (0) on chain 1.
-
Open the
election-dapp/frontend
folder in a terminal shell on your computer. -
Install the frontend dependencies by running the following command:
npm install
npm install
-
Start the frontend application configured to use the
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 there's at least one candidate listed. -
Click Set Account.
-
Copy and paste the voter account name from Chainweaver into the election application, then click Save.
-
Click Vote Now for a candidate, sign the transaction, then open the Developer Tools for your browser and view the console output.
In the console, you'll see an error similar to the following:
Attempt to buy gas failed with: (enforce (<= amount balance) "...: Failure: Tx Failed: Insufficient funds
Attempt to buy gas failed with: (enforce (<= amount balance) "...: Failure: Tx Failed: Insufficient funds
With the current implementation, it isn't possible to vote using an account that has a zero balance.
Implement the gas payer interface
In this tutorial, you'll add a second Pact module—the election-gas-station
module—to your election
smart contract.
To create the gas station module:
-
Open the
election-dapp/pact
folder in the code editor on your computer. -
Create a new
election-gas-station.pact
file in thepact
folder. -
Add the minimal Pact code required to define a module.
Remember that a module definition requires a namespace, a governing owner, and at least one function. In this case, the function you want to add to the module is an implementation of the
gas-payer-v1
interface. Because you're deploying the module in your own principal namespace on the local development network, be sure you replace the namespace and keyset with the principal namespace you defined on the development network.For example:
(namespace 'n_14912521e87a6d387157d526b281bde8422371d1) (module election-gas-station GOVERNANCE (defcap GOVERNANCE () (enforce-keyset "n_14912521e87a6d387157d526b281bde8422371d1.admin-keyset") ) (implements gas-payer-v1))
(namespace 'n_14912521e87a6d387157d526b281bde8422371d1) (module election-gas-station GOVERNANCE (defcap GOVERNANCE () (enforce-keyset "n_14912521e87a6d387157d526b281bde8422371d1.admin-keyset") ) (implements gas-payer-v1))
As you can see in this example, the new module—like the
election
module—is governed by youradmin-keyset
. -
Create a
election-gas-station.repl
file in thepact
folder and add the following lines of code:(load "setup.repl") (begin-tx "Load election gas station module") (load "root/gas-payer-v1.pact") (load "election-gas-station.pact")(commit-tx)
(load "setup.repl") (begin-tx "Load election gas station module") (load "root/gas-payer-v1.pact") (load "election-gas-station.pact")(commit-tx)
-
Execute the transaction in the Pact REPL running locally or in the Docker container.
If the Pact REPL is installed locally, run the following command inside the
pact
folder in the terminal shell:pact election-gas-station.repl --trace
pact election-gas-station.repl --trace
As before, if you don't have the Pact REPL installed locally, you can load the file in the Pact REPL with the following command:
(load "election-gas-station.repl")
(load "election-gas-station.repl")
If you are using the Pact REPL in a browser, you can replace the
pact election-gas-station.repl --trace
command with(load "election-gas-station.repl")
throughout this tutorial.You should see that this transaction fails with an error similar to the following:
election-gas-station.pact:3:3:Error: found unimplemented member while resolving model constraints: GAS_PAYER at election-gas-station.pact:3:3: moduleLoad failed
election-gas-station.pact:3:3:Error: found unimplemented member while resolving model constraints: GAS_PAYER at election-gas-station.pact:3:3: moduleLoad failed
The
gas-payer-v1
interface you have referenced in yourelection-gas-station.pact
file is defined in theelection-dapp/pact/root/gas-payer-v1.pact
file. This file is included in your project so that you can test your module in the Pact REPL. The interface is also pre-installed on the Kadena development, test, and main networks, so you don't need to deploy it when you deploy theelection-gas-station
module. However, you haven't implemented thegas-payer-v1
interface yet in theelection-gas-station.pact
file. -
Open the
election-dapp/pact/root/gas-payer-v1.pact
file in the code editor on your computer and review the signature for the interface.The documentation for the
gas-payer-v1
interface file states thatGAS_PAYER
should compose a capability. You can include a capability within a capability using the built-incompose-capability
function. From this documentation, you know that you need to add theALLOW_GAS
capability that always returnstrue
within theGAS_PAYER
capability to implement thegas-payer-v1
interface. -
Add the capability
ALLOW_GAS
within theGAS_PAYER
capability in theelection-gas-station.pact
file with the following lines of code:(defcap GAS_PAYER:bool ( user:string limit:integer price:decimal ) (compose-capability (ALLOW_GAS))) (defcap ALLOW_GAS () true)
(defcap GAS_PAYER:bool ( user:string limit:integer price:decimal ) (compose-capability (ALLOW_GAS))) (defcap ALLOW_GAS () true)
-
Execute the transaction using the
pact
command-line program:pact election-gas-station.repl --trace
pact election-gas-station.repl --trace
You should see that this transaction fails with an error similar to the following:
election-gas-station.pact:3:3:Error: found unimplemented member while resolving model constraints: create-gas-payer-guard at election-gas-station.pact:3:3: moduleLoad failed
election-gas-station.pact:3:3:Error: found unimplemented member while resolving model constraints: create-gas-payer-guard at election-gas-station.pact:3:3: moduleLoad failed
If you review the
gas-payer-v1
interface again, you'll see it defines acreate-gas-payer-guard
function that you haven't implemented yet in yourelection-gas-station
module. To implement the required guard, you can use the built-increate-capability-guard
function and pass theALLOW_GAS
capability into it. The function returns a guard for theALLOW_GAS
capability. -
Add the
create-capability-guard
function and pass theALLOW_GAS
capability into it with the following lines of code:(defun create-gas-payer-guard:guard () (create-capability-guard (ALLOW_GAS)) )
(defun create-gas-payer-guard:guard () (create-capability-guard (ALLOW_GAS)) )
-
Execute the transaction using the
pact
command-line program:pact election-gas-station.repl --trace
pact election-gas-station.repl --trace
You should see that the transaction succeeds with output similar to the following:
election-gas-station.pact:3:3:Trace: Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station, hash UKFa_ybmNJeGY1JJHtz4mv5h5QaN6-29WMIa4H6SIz8election-gas-station.repl:6:0:Trace: Commit Tx 3: Load election gas station moduleLoad successful
election-gas-station.pact:3:3:Trace: Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station, hash UKFa_ybmNJeGY1JJHtz4mv5h5QaN6-29WMIa4H6SIz8election-gas-station.repl:6:0:Trace: Commit Tx 3: Load election gas station moduleLoad successful
Now that you have a working implementation of the
gas-payer-v1
interface, you can deploy the new module on the development network to test whether it can pay the transaction fee for votes cast using the election application.
Deploy the Pact module on the development network
To deploy the new Pact 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 deploys the module.
-
Open the
election-dapp/snippets
folder in a terminal shell on your computer. -
Deploy your
election-gas-station
module on the development network by running a command similar to the following with your administrative account name:npm run deploy-gas-station:devnet -- k:<your-public-key>
npm run deploy-gas-station:devnet -- k:<your-public-key>
The
election-dapp/deploy-gas-station.ts
script is similar to theelection-dapp/deploy-module.ts
script, except that it deploys theelection-gas-station.pact
module. Remember thatk:<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.When you run the script, you should see Chainweaver display a QuickSign Request.
-
Click Sign All 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: 60414, result: { status: 'success', data: 'Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station, hash UKFa_ybmNJeGY1JJHtz4mv5h5QaN6-29WMIa4H6SIz8' }, reqKey: '0b0yxjVLgKusW5obhDJON6jww1BF0cTfn3O2aiffV7U', logs: 'lYZH-dn07T7PmnxUMf-h4vch8sPoHfz42olDtV153fA', events: [ { params: [Array], name: 'TRANSFER', module: [Object], moduleHash: 'M1gabakqkEi_1N8dRKt4z5lEv1kuC_nxLTnyDCuZIK0' } ], metaData: { publicMeta: { creationTime: 1706218447, ttl: 28800, gasLimit: 100000, chainId: '1', gasPrice: 1e-8, sender: 'k:5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0' }, blockTime: 1706218445726808, prevBlockHash: 'so-M2Qv_sPH9se6OigQEfrznrQgl6H5XTI5xMdXK-TY', blockHeight: 14684 }, continuation: null, txId: 14728, preflightWarnings: []}{ status: 'success', data: 'Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station, hash UKFa_ybmNJeGY1JJHtz4mv5h5QaN6-29WMIa4H6SIz8'}
{ gas: 60414, result: { status: 'success', data: 'Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station, hash UKFa_ybmNJeGY1JJHtz4mv5h5QaN6-29WMIa4H6SIz8' }, reqKey: '0b0yxjVLgKusW5obhDJON6jww1BF0cTfn3O2aiffV7U', logs: 'lYZH-dn07T7PmnxUMf-h4vch8sPoHfz42olDtV153fA', events: [ { params: [Array], name: 'TRANSFER', module: [Object], moduleHash: 'M1gabakqkEi_1N8dRKt4z5lEv1kuC_nxLTnyDCuZIK0' } ], metaData: { publicMeta: { creationTime: 1706218447, ttl: 28800, gasLimit: 100000, chainId: '1', gasPrice: 1e-8, sender: 'k:5ec41b89d323398a609ffd54581f2bd6afc706858063e8f3e8bc76dc5c35e2c0' }, blockTime: 1706218445726808, prevBlockHash: 'so-M2Qv_sPH9se6OigQEfrznrQgl6H5XTI5xMdXK-TY', blockHeight: 14684 }, continuation: null, txId: 14728, preflightWarnings: []}{ status: 'success', data: 'Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station, hash UKFa_ybmNJeGY1JJHtz4mv5h5QaN6-29WMIa4H6SIz8'}
With this transaction, you now have two Pact modules in your
election
smart contract. -
Verify that the
election-gas-station
module is deployed on the development network by running the following command:npm run list-modules:devnet
npm run list-modules:devnet
You should see your modules listed in output similar to the following:
'n_14912521e87a6d387157d526b281bde8422371d1.election','n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station',
'n_14912521e87a6d387157d526b281bde8422371d1.election','n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station',
Update the vote function
The next step is to ensure that the signature of the account that votes is within the scope of the GAS_PAYER
capability. To do this, you'll update the vote
function to accept the following arguments:
- The voter account name.
- Zero as the gas limit to allow unlimited gas.
- Zero as the gas price.
You'll also change the senderAccount
in the transaction metadata to use the election-gas-station
module so that the election gas station account pays the transaction fee for voting transactions instead of the voter account.
To update the vote
function:
-
Open the
frontend/src/repositories/vote/DevnetVoteRepository.ts
file in the code editor on your computer. -
Update the
vote
function to change the.addSigner(accountKey(account))
code as follows:.addSigner(accountKey(account), (withCapability) => [ withCapability(`${NAMESPACE}.election-gas-station.GAS_PAYER`, account, { int: 0 }, { decimal: '0.0' }),])
.addSigner(accountKey(account), (withCapability) => [ withCapability(`${NAMESPACE}.election-gas-station.GAS_PAYER`, account, { int: 0 }, { decimal: '0.0' }),])
-
Update the
senderAccount
in the transaction metadata to beelection-gas-station
as follows:.setMeta({ chainId: CHAIN_ID, ttl: 28000, gasLimit: 100000, gasPrice: 0.000001, senderAccount: 'election-gas-station',})
.setMeta({ chainId: CHAIN_ID, ttl: 28000, gasLimit: 100000, gasPrice: 0.000001, senderAccount: 'election-gas-station',})
If you have closed the election application you previously had running, restart it using the
devnet
backend, then openhttp://localhost:5173
in your browser. -
Click Set Account, copy and paste the voter account name from Chainweaver to vote using that account, then click Save.
-
Click Vote Now for a candidate, sign the transaction, then open the Developer Tools for your browser and view the console output.
In the console, you'll see an error similar to the following:
Uncaught (in promise) Error: Validation failed for hash "shH9LgwlSuvMtm2hR-LvxFTYUOOA-iw359d4y45xO7M": Attempt to buy gas failed with: (read coin-table sender): Failure: Tx Failed: read: row not found: election-gas-station
Uncaught (in promise) Error: Validation failed for hash "shH9LgwlSuvMtm2hR-LvxFTYUOOA-iw359d4y45xO7M": Attempt to buy gas failed with: (read coin-table sender): Failure: Tx Failed: read: row not found: election-gas-station
As this error indicates, the
election-gas-station
account you specified for thesenderAccount
doesn't exist yet. You need to create and fund the account before it can be used by voters.
Create the gas station account
To make the gas station account more secure, you can create it using a principal account name and guard access to it by using the ALLOW_GAS
capability. Because the gas station account is a capability-guarded account, you can use the create-principal
Pact function to automatically create its account name with a c:
prefix. You can then define the gas station account name as a constant in the election-gas-station.pact
file.
To create a capability-guarded account:
-
Open the
election-dapp/pact
folder in the code editor on your computer. -
Open the
election-gas-station.pact
file and add the following line of code to the end of the module definition:(defconst GAS_STATION_ACCOUNT (create-principal (create-gas-payer-guard)))
(defconst GAS_STATION_ACCOUNT (create-principal (create-gas-payer-guard)))
-
Open the
./pact/election-gas-station.repl
file and update the transaction to display the capability-guarded gas station account name when you run the file.(load "setup.repl") (begin-tx "Load election gas station module") (load "root/gas-payer-v1.pact") (load "election-gas-station.pact") [GAS_STATION_ACCOUNT](commit-tx)
(load "setup.repl") (begin-tx "Load election gas station module") (load "root/gas-payer-v1.pact") (load "election-gas-station.pact") [GAS_STATION_ACCOUNT](commit-tx)
-
Execute the transaction using the
pact
command-line program:pact election-gas-station.repl --trace
pact election-gas-station.repl --trace
You should see that the transaction succeeds with output similar to the following:
election-gas-station.repl:5:2:Trace: Loading election-gas-station.pact...election-gas-station.pact:1:0:Trace: Namespace set to n_14912521e87a6d387157d526b281bde8422371d1election-gas-station.pact:3:3:Trace: Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station, hash -idAeKp54xkfddZ9MIxQw8GCD4jTZ_Ow8pXWR9zwC-kelection-gas-station.repl:6:0:Trace: Commit Tx 3: Load election gas station moduleLoad successful
election-gas-station.repl:5:2:Trace: Loading election-gas-station.pact...election-gas-station.pact:1:0:Trace: Namespace set to n_14912521e87a6d387157d526b281bde8422371d1election-gas-station.pact:3:3:Trace: Loaded module n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station, hash -idAeKp54xkfddZ9MIxQw8GCD4jTZ_Ow8pXWR9zwC-kelection-gas-station.repl:6:0:Trace: Commit Tx 3: Load election gas station moduleLoad successful
-
Open the
election-gas-station.pact
file in the code editor on your computer. -
Add an
init
function that uses thecreate-account
function from thecoin
module to create the gas station account in theelection-gas-station
module:(defun init () (coin.create-account GAS_STATION_ACCOUNT (create-gas-payer-guard)))
(defun init () (coin.create-account GAS_STATION_ACCOUNT (create-gas-payer-guard)))
In this code:
- The first argument of the function is the account name you just defined.
- The second argument is the guard for the account.
-
Add an if-statement after the module definition that calls the
init
function if the module is deployed with{ "init": true }
in the transaction data:(if (read-msg 'init) [(init)] ["not creating the gas station account"])
(if (read-msg 'init) [(init)] ["not creating the gas station account"])
-
Update the
election-gas-station.repl
file to setinit
to true for the next transaction by adding the following lines of code after loading thesetup.repl
module:(env-data { 'init: true })
(env-data { 'init: true })
-
Execute the transaction using the
pact
command-line program:pact election-gas-station.repl --trace
pact election-gas-station.repl --trace
You should see that the transaction succeeds with output similar to the following:
election-gas-station.pact:30:0:Trace: ["Write succeeded"]election-gas-station.repl:10:2:Trace: ["c:qjp3-APtX5tTTSvQSMbJ1KZ1hCru238IUirIqN6tkMI"]election-gas-station.repl:11:0:Trace: Commit Tx 3: Load election gas station moduleLoad successful
election-gas-station.pact:30:0:Trace: ["Write succeeded"]election-gas-station.repl:10:2:Trace: ["c:qjp3-APtX5tTTSvQSMbJ1KZ1hCru238IUirIqN6tkMI"]election-gas-station.repl:11:0:Trace: Commit Tx 3: Load election gas station moduleLoad successful
If you're successful loading the
election-gas-station module
in the Pact REPL, you can update the module deployed on the development network.
Update the gas station module
To deploy the new Pact 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 a terminal shell on your computer. -
Deploy your
election-gas-station
module on the development network by running a command similar to the following with your administrative account name:npm run deploy-gas-station:devnet -- k:<your-public-key> upgrade init
npm run deploy-gas-station:devnet -- k:<your-public-key> upgrade init
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. As before, you must includeupgrade
andinit
to update the contract and add the GAS_STATION_ACCOUNT capability guard from yourelection-gas-station
module. -
Click Sign All to sign the request.
After you click Sign All, the transaction is executed and the results are displayed in your terminal shell. For example, you should see output similar to the following:
{ status: 'success', data: [ 'Write succeeded' ] }
{ status: 'success', data: [ 'Write succeeded' ] }
You can also verify that the gas station account exists with a 0 balance on the development network by running the following script:
npm run coin-details:devnet -- c:<capability-guarded-account-name>
npm run coin-details:devnet -- c:<capability-guarded-account-name>
Replace
c:<capability-guarded-account-name>
with the gas station account name displayed when you tested theelection-gas-station.repl
file in the Pact REPL.After running the script, you should see output similar to the following:
{ guard: { cgPactId: null, cgArgs: [], cgName: 'n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station.ALLOW_GAS' }, balance: 0, account: 'c:qjp3-APtX5tTTSvQSMbJ1KZ1hCru238IUirIqN6tkMI'}
{ guard: { cgPactId: null, cgArgs: [], cgName: 'n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station.ALLOW_GAS' }, balance: 0, account: 'c:qjp3-APtX5tTTSvQSMbJ1KZ1hCru238IUirIqN6tkMI'}
In the account details, you can see that the
ALLOW_GAS
capability is used to guard the gas station account. TheALLOW_GAS
capability has a prefix that includes your principal namespace and the module name.Because the principal namespace is based on your administrative keyset and the principal account of the gas station is based on a capability including that principal namespace, you know that the gas station account name is unique to your administrative account. This account naming scheme makes it impossible for someone with a different keyset to use your gas station account on another chain. As a result, principal accounts in principal namespaces are far more secure than vanity account names in the
free
namespace.
Fund the gas station account
Now that you have created and deployed a secure gas station account, you're ready to fund the account to pay transaction fees.
To fund the gas station account:
-
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 funds the gas station account.
-
Open the
election-dapp/snippets
folder in a terminal shell on your computer. -
Transfer one KDA from your administrative account to the gas station account by running the following command:
npm run transfer:devnet -- k:<your-public-key> c:<capability-guarded-account-name> 1
npm run transfer:devnet -- k:<your-public-key> c:<capability-guarded-account-name> 1
Remember to replace
k:<your-public-key>
with the account name for your administrative account andc:<capability-guarded-account-name>
with the account name for your gas station. Thetransfer.ts
script is similar to thetransfer-create.ts
script except that this script:- Transfers KDA from your administrative account and must be signed using Chainweaver.
- Requires the receiving account to already exist on the blockchain.
-
Click Sign All to sign the request.
After you click Sign All, the transaction is executed and the results are displayed in your terminal shell. For example, you should see output similar to the following:
{ status: 'success', data: [ 'Write succeeded' ] }
{ status: 'success', data: [ 'Write succeeded' ] }
-
Verify that the election gas station account now has a KDA balance on the development network by running the following script:
npm run coin-details:devnet -- c:<capability-guarded-account-name>
npm run coin-details:devnet -- c:<capability-guarded-account-name>
Remember to replace
c:<capability-guarded-account-name>
with the account name for your gas station.After running the script, you should see output similar to the following:
{ guard: { cgPactId: null, cgArgs: [], cgName: 'n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station.ALLOW_GAS' }, balance: 1, account: 'c:qjp3-APtX5tTTSvQSMbJ1KZ1hCru238IUirIqN6tkMI'}
{ guard: { cgPactId: null, cgArgs: [], cgName: 'n_14912521e87a6d387157d526b281bde8422371d1.election-gas-station.ALLOW_GAS' }, balance: 1, account: 'c:qjp3-APtX5tTTSvQSMbJ1KZ1hCru238IUirIqN6tkMI'}
Modify the senderAccount
Now that you have created a capability-guarded account for the gas station, you need to modify the vote function to use this account.
To modify the senderAccount
to use the gas station account:
-
Open the
frontend/src/repositories/vote/DevnetVoteRepository.ts
file in the code editor on your computer. -
Update the
senderAccount
in the transaction metadata to replace'election-gas-station'
with thec:<capability-guarded-account-name>
account name for your gas station.For example:
.setMeta({ chainId: CHAIN_ID, ttl: 28000, gasLimit: 100000, gasPrice: 0.000001, senderAccount: 'c:qjp3-APtX5tTTSvQSMbJ1KZ1hCru238IUirIqN6tkMI',})
.setMeta({ chainId: CHAIN_ID, ttl: 28000, gasLimit: 100000, gasPrice: 0.000001, senderAccount: 'c:qjp3-APtX5tTTSvQSMbJ1KZ1hCru238IUirIqN6tkMI',})
Set the scope for signatures
At this point, most of the work required to use a gas station to pay transaction fees is done. However, if you attempt to vote in the election application and sign the transaction with the voter account name from Chainweaver, the Developer Tools console output will display an error similar to the following:
App.tsx:42 Uncaught (in promise) {callStack: Array(0), type: 'TxFailure', message: 'Keyset failure (keys-all): [bbccc99e...]', info: ''}
App.tsx:42 Uncaught (in promise) {callStack: Array(0), type: 'TxFailure', message: 'Keyset failure (keys-all): [bbccc99e...]', info: ''}
When you added the ACCOUNT-OWNER
capability to the election-dapp/pact/election.pact
file, you didn't set the scope for the capability.
You might recall in the previous tutorial that you tested voting with a transaction similar to the following in the voting.repl
file:
(env-sigs [{ 'key : "voter" , 'caps : [] }]) (begin-tx "Vote as voter") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (vote "voter" "1") (expect "Candidate A has 2 votes" 2 (at 'votes (at 0 (list-candidates))) )(commit-tx)
(env-sigs [{ 'key : "voter" , 'caps : [] }]) (begin-tx "Vote as voter") (use n_14912521e87a6d387157d526b281bde8422371d1.election) (vote "voter" "1") (expect "Candidate A has 2 votes" 2 (at 'votes (at 0 (list-candidates))) )(commit-tx)
In this test from the previous tutorial, the caps
field passed to env-sigs
is an empty array.
As a consequence, the signature of the transaction is not scoped to any capability and the signer automatically approves all capabilities required for the function to execute.
However, in this tutorial, you modified the vote
function in the frontend/src/repositories/vote/DevnetVoteRepository.ts
file to scope the signature of the vote
transaction to grant the GAS_PAYER
capability, but not the ACCOUNT-OWNER
capability.
If you sign for some capabilities but not for all capabilities required for a transaction to be executed, the transaction will fail at the point where a capability is required that you didn't sign for.
Therefore, you need to add a second capability to the array passed to addSigners
in the vote
function.
To set the scope for the ACCOUNT-OWNER
capability:
-
Open the
frontend/src/repositories/vote/DevnetVoteRepository.ts
file in the code editor on your computer. -
Add the
ACCOUNT-OWNER
capability to the.addSigner
with the following line of code:withCapability(`${NAMESPACE}.election.ACCOUNT-OWNER`, account),
withCapability(`${NAMESPACE}.election.ACCOUNT-OWNER`, account),
Cast a vote
To cast a vote with the voter account:
-
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 voter account name with the k: prefix exists on chain 1.
- Your voter account name has no KDA account balance (0) on chain 1.
If you have closed the election application you previously had running:
- Open the
election-dapp/frontend
folder in a terminal shell on your computer. - Install the frontend dependencies by running the
npm install
command. - Start the frontend application configured to use the
devnet
backend by running thenpm run start-devnet
command.
-
Open
http://localhost:5173
in your browser and verify that there's at least one candidate listed. -
Click Set Account, copy and paste the voter account name from Chainweaver to vote using that account, then click Save.
-
Click Vote Now for a candidate, sign the transaction, and wait for it to complete.
You should see the vote count for the candidate you voted for incremented by one vote.
Enforce a limit on transaction fees
You now have a functioning gas station for the election application. However, you might want to make some additional changes to make the module more secure. For example, you should enforce an upper limit for transaction fees to help ensure that funds in the gas station account aren't drained too quickly.
To set an upper limit for transaction fees:
-
Open the
election-gas-station.pact
file in the code editor on your computer. -
Add the following function to retrieve the gas price from the metadata of the transaction using the built-in
chain-data
function:(defun chain-gas-price () (at 'gas-price (chain-data)))
(defun chain-gas-price () (at 'gas-price (chain-data)))
-
Add the following function to force the gas price to be below a specified limit.
(defun enforce-below-or-at-gas-price:bool (gasPrice:decimal) (enforce (<= (chain-gas-price) gasPrice) (format "Gas Price must be lower than or equal to {}" [gasPrice])))
(defun enforce-below-or-at-gas-price:bool (gasPrice:decimal) (enforce (<= (chain-gas-price) gasPrice) (format "Gas Price must be lower than or equal to {}" [gasPrice])))
-
Update the
GAS_PAYER
capability by adding(enforce-below-or-at-gas-price 0.000001)
right before(compose-capability (ALLOW_GAS))
.For example:
(enforce-below-or-at-gas-price 0.000001)(compose-capability (ALLOW_GAS))
(enforce-below-or-at-gas-price 0.000001)(compose-capability (ALLOW_GAS))
Set limits on the transactions allowed
In its current state, any module can use your gas station to pay for any type of transaction, including transactions that involve multiple steps and could be quite costly. For example, a cross-chain transfer is a transaction that requires a continuation with part of the transaction taking place on the source chain and completed on the destination chain. This type of "continued" transaction requires more computational resources—that is, more gas—than a simple transaction that completes in a single step.
To prevent the gas station account from being depleted by transactions that require multiple steps, you can configure the gas station module to only allow simple transactions, identified by the exec
transaction type. Transactions identified with the exec
transaction type can contain multiple functions but complete in a single step.
To set limits on the transactions allowed to access the gas station account:
-
Open the
election-gas-station.pact
file in the code editor on your computer. -
Restrict the transaction type to only allow
exec
transactions by adding the following line to the start of theGAS_PAYER
capability definition:(enforce (= "exec" (at "tx-type" (read-msg))) "Can only be used inside an exec")
(enforce (= "exec" (at "tx-type" (read-msg))) "Can only be used inside an exec")
An
exec
transaction can contain multiple function calls. You can further restrict access to the funds in the gas station account by only allowing specific function calls.An
exec
transaction can contain multiple function calls. You can also restrict access to the gas station account by only allowing specific function calls. -
Restrict access to only allow one function call by adding the following line to the
GAS_PAYER
capability definition:(enforce (= 1 (length (at "exec-code" (read-msg)))) "Can only be used to call one pact function")
(enforce (= 1 (length (at "exec-code" (read-msg)))) "Can only be used to call one pact function")
-
Restrict access to only pay transaction fees for functions defined in the
election
module by adding the following line to theGAS_PAYER
capability definition:(enforce (= "(n_14912521e87a6d387157d526b281bde8422371d1.election." (take 52 (at 0 (at "exec-code" (read-msg))))) "Only election module calls are allowed")
(enforce (= "(n_14912521e87a6d387157d526b281bde8422371d1.election." (take 52 (at 0 (at "exec-code" (read-msg))))) "Only election module calls are allowed")
Remember to replace the namespace with your own principal namespace.
Update the smart contract on the development network
After you've completed the changes to secure the gas station account, you are ready to update the smart contract you have deployed on the development network and complete the workshop.
To update the smart contract and complete the workshop:
-
Open the
election-dapp/pact
folder in a terminal shell on your computer and verify all of the tests you created in the workshop pass using the Pact REPL.- pact/candidates.repl
- pact/election-gas-station.repl
- pact/keyset.repl
- pact/module.repl
- pact/namespace.repl
- pact/principal-namespace.repl
- pact/setup.repl
-
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.
-
Open the
election-dapp/snippets
folder in a terminal shell on your computer. -
Update your
election-gas-station
module on the development network by running a command similar to the following with your administrative account name:npm run deploy-gas-station:devnet -- k:<your-public-key> upgrade
npm run deploy-gas-station:devnet -- k:<your-public-key> upgrade
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. When you run the script, you should see Chainweaver display a QuickSign Request. -
Click Sign All to sign the request.
After you click Sign All, the transaction is executed and the results are displayed in your terminal shell.
-
Verify your contract changes in the Chainweaver Module Explorer by refreshing the list of Deployed Contracts, then clicking View for the
election-gas-station
module.After you click View, you should see the updated list of functions and capabilities. If you click Open, you can view the module code in the editor pane and verify that the
election-gas-station
module deployed on the local development network is what you expect.
Next steps
In this tutorial, you learned how to:
- Add a second module to your smart contract.
- Define a gas station account that pays transaction fees on behalf of other accounts.
- Restrict access to the gas station account based on conditions you specify in the Pact module.
- Deploy the gas station module on the development network.
In this workshop, you configured an election application to use the Kadena client to interact with a smart contract deployed on the Kadena blockchain as its backend. The workshop demonstrates the basic functionality for conducting an election online that uses a blockchain to provide more efficient, transparent, and tamper-proof results. However, as you saw in Add vote management, it's possible for individuals to vote more than once by simply creating additional Kadena accounts. That might be a challenge you want to explore.
As an alternative, you might want to deploy the election application and smart contract on the Kadena test network, making it available to community members.
We can't wait to see what you build next.
To see the code for the activity you completed in this tutorial, check out the 00-complete
branch from the voting-dapp
repository by running the following command in your terminal shell:
git checkout 00-complete
git checkout 00-complete