Today, I will guide to write voting app on NEAR blockchain. The tutorial will consist of three parts:
- How to writing a voting smart contract on NEAR using AssemblyScript. The Smart Contract will ensure the following requirements:
- Support multiple voting
- Support to add multiple candidates for each voting
- Anyone with a NEAR wallet can vote on one candidate once
- View all the information of the voting (Includes all the candidates and the number of votes they have)
- How to deploy the voting smart contract to NEAR Testnet
- How to build the frontend to create complete voting app
Now let’s start!
Mục lục
Prepare the environment
Firstly, you need to prepare the working environment in advance:
- NodeJs: You need to install NodeJs version 14 or higher. and then install yarn:
npm i -g yarn - near-sdk-as: We will write the contract in AssemblyScript language, so we need to install near-sdk-as with the following command:
yarn add -D near-sdk-as - near-cli: A tool that provides an interface for manipulating the NEAR blockchain. We use it to deploy Smart Contract and call Smart Contract functions, and it does many other things. You can see more details at NEAR-CLI. You use the following command to install:
npm i -g near-cli
Now we create the voting app with the following command:
// Create voting-app
yarn create near-app voting-app
// Go to the project foler
cd voting-app
// Install libraries
yarn
In the project directory, you will find the following subfolders:
Now we run the test with the command:
yarn dev
If you see the browser open the link http://localhost:1234, that means everything is ok!
All preparations are done. Now we will write writing a voting smart contract.
Writing a voting smart contract on NEAR using AssemblyScript
We write a smart contract, so we will be interested in the contract directory as below image. The contract/assembly/index.ts file is the file that contains our main smart contract code.
Before writing a smart contract, we need to define the data to be stored on the blockchain:
- We support many candidates, each candidate will have information: Name, numer of vote. We store more id field for easy identification of candidates. So we need declare a CandidateItem data structure.
- We support many voting, each voting will have information:
- id
- owner: The account created this voting
- status: Determine the state of voting, get the following values:
- 0: New Voting created, you can add candidates
- 1: The voting is running. Everyone can vote for the candidates.
- 2: The voting is closed.
- candidates: List of candidates
- startTime, startBlock , endTime, endBlock: More information for this voting
Now we will create a file contract/assembly/model.ts to declare the data stored on the Blockchain. The content of this file is as follows:
import { env, PersistentVector, PersistentMap } from "near-sdk-as";
// Declare data structure of a candidate
@nearBindgen
export class Candidate {
id: i32;
name: string;
vote: u32;
constructor(_id: i32, _name: string) {
this.id = _id;
this.name = _name;
this.vote = 0;
}
}
// Declare data structure of a voting
@nearBindgen
export class Voting {
id: i32;
owner: string; // The account created this voting
content: string; // The content of this voting
status: i32; // 0: New - 1: Running - 2: Close
startTime: u64;
startBlock: u64;
endTime: u64;
endBlock: u64;
candidates: Candidate[];
// Contructor function
constructor(_id: i32, _owner: string, _content: string) {
this.id = _id;
this.owner = _owner;
this.content = _content;
this.status = 0;
this.startTime = 0;
this.startBlock = 0;
this.endTime = 0;
this.endBlock = 0;
this.candidates = [];
}
// Allow to start voting
startVote(): void {
this.status = 1;
this.startTime = env.block_timestamp();
this.startBlock = env.block_index();
}
// Close voting
endVote(): void {
this.status = 2;
this.endTime = env.block_timestamp();
this.endBlock = env.block_index();
}
// Add a candidate to the voting
addCandidate(candidate: string): void {
let id = this.candidates.length;
let item = new Candidate(id, candidate);
this.candidates.push(item);
}
// Check a candidate is existed?
isCandidateExisted(candidate: string): bool {
for (let idx=0; idx<this.candidates.length; idx++) {
if (this.candidates[idx].name==candidate) return true;
}
return false;
}
// Increase vote for a candidate
increaseVote(candidateId: i32): void {
this.candidates[candidateId].vote++;
}
}
// Store the information of votings to the blockchain
export const votingInfos = new PersistentVector<Voting>("voting_infos");
// Store the information of the elected accounts
// We use it to check to make sure the account only votes once
export const votingUsers = new PersistentMap<i32, PersistentMap<string, string>>("voting_users");
Here I have some notes for you:
- We notice the keyword @nearBindgen. We use it for data structures that need to be saved to the blockchain.
- We have to save more information of the accounts who voted in the variable votingUsers. That way we can know which accounts participated in the vote and which candidate they voted for..
- We have to use PersistentVector and PersistentMap of near-sdk-as library to save data to Blockchain
Now we will implement the main contract functions in the file contract/assembly/index.ts. Please delete all the contents of this file and replace it with the following content:
import { Context, PersistentMap, logging, u128, env } from 'near-sdk-as'
import { Voting, votingInfos, votingUsers } from './model';
// Users who want to vote need to deposit at least 0.1 NEAR
// This will help prevent users from spamming
const MIN_VOTE_AMOUNT = u128.from("100000000000000000000000");
// Get current voting
function _getVotingInfo(): Voting | null {
let len = votingInfos.length;
if (len>0) return votingInfos[len-1];
return null;
}
// Allows users to create new voting
// Don't check user permissions so anyone can create
// It's convenient that other people can run the contract as well.
// But for real implementation, you have to check the permissions, only the owner has the right to create votes
export function createVoting(content: string): bool {
// Checking
assert(content, "You must enter the content!");
let currVote = _getVotingInfo();
if (currVote) {
assert(currVote.status==2, "The vote is not ended. You can not create new voting!");
}
// Create new vote and store into blockchain
// The id of the vote is its position in the array
let id = votingInfos.length;
let item = new Voting(id, Context.sender, content);
item.startVote(); // Reduce action for users
votingInfos.push(item);
// Create votingUser and store it into blockchain
let votingUser = new PersistentMap<string, string>(`voting_users_${id}`);
votingUsers.set(id, votingUser);
return true;
}
// Allows users to add candidate to the current voting
export function addCandidate(candidate: string): bool {
// Checking
let currVote = _getVotingInfo();
assert(currVote!=null, "There is no voting!");
assert(candidate!="", "Invalid input");
if (currVote) {
// Checking more
assert(currVote.status==0 || currVote.status==1, "The vote is ended. You can not add candidate!");
// assert(currVote.owner==Context.sender, "You is not owner of current vote!");
assert(!currVote.isCandidateExisted(candidate), "The candidate is existed!");
// Add candidate to the current voting and store it into blockchain
currVote.addCandidate(candidate);
votingInfos.replace(votingInfos.length-1, currVote);
return true;
}
return false;
}
// Allow to start voting
export function startVote(): bool {
// Checking
let currVote = _getVotingInfo();
assert(currVote!=null, "There is no voting!");
if (currVote) {
// Checking more
assert(currVote.status==0, "The vote is running or ended. You can not start voting!");
// assert(currVote.owner==Context.sender, "You is not owner of current vote!");
// Update current voting and store it into blockchain
currVote.startVote();
votingInfos.replace(votingInfos.length-1, currVote);
}
return true;
}
// Close voting
export function endVote(): bool {
// Checking
let currVote = _getVotingInfo();
assert(currVote!=null, "There is no voting!");
if (currVote) {
// Checking more
assert(currVote.status==1, "The vote is not running. You can not end voting!");
// assert(currVote.owner==Context.sender, "You is not owner of current vote!");
// Update current voting and store it into blockchain
currVote.endVote();
votingInfos.replace(votingInfos.length-1, currVote);
}
return true;
}
// Users vote for their favorite candidate
export function vote(candidateId: i32): bool {
// Checking
let currVote = _getVotingInfo();
assert(currVote!=null, "There is no voting!");
if (currVote) {
// Checking more
assert(currVote.status==1, "The vote is not running. You can not vote!");
// assert(currVote.owner!=Context.sender, "The owner has not right to vote!");
assert(candidateId>=0 && candidateId<currVote.candidates.length, "The candidateId is invalid!");
// Check deposit
let attachedDeposit = Context.attachedDeposit;
assert(u128.ge(attachedDeposit, MIN_VOTE_AMOUNT), "You must deposit 0.5 NEAR to vote!");
// Checking account
let votingUser = votingUsers.get(currVote.id);
assert(votingUser!=null, "Invalid data!");
if (votingUser) {
// Checking more
assert(votingUser.get(Context.sender)==null, "You has voted before!");
// Update votingUser and store it into blockchain
votingUser.set(Context.sender, `${candidateId}`);
votingUsers.set(currVote.id, votingUser);
// Increase vote for the candidate and store it into blockchain
currVote.increaseVote(candidateId);
votingInfos.replace(votingInfos.length-1, currVote);
return true;
}
}
return false;
}
// Get information of current voting / lastest voting
export function votingInfo(): Voting | null {
return _getVotingInfo();
}
// Get information of any voting
export function votingInfoById(votingId: i32): Voting | null {
let len = votingInfos.length;
if (len>0 && votingId>=0 && votingId<votingInfos.length) return votingInfos[votingId];
return null;
}
You can read comments to understand the code. Here I make a few notes:
- We use assert() function to check data. If the data is incorrect, the transaction will fail and the user will be refunded. And if we check normally, if the data is not correct, return false. At this time, the transaction on the blockchain is still successful and the user will lose the deposit.
- In this contract, I don’t check user permission so anyone can test this contract. In fact for actual implementation you have to check permissions carefully.
- To minimize user manipulation, the createVoting() function will also call the startVote() function => The user needn’t to call startVote()
- For testing convenience, during voting, users can still add candidates.
- Voting flow as following:
createVoting -> addCandidate() + vote() -> endVote()
OK. We have finished writing the contract. Now let’s try to build to see if there are any problems? Use the following command to build:
yarn build:contract
Bạn thấy như ảnh dưới là build thành công rùi nhé. Và bạn sẽ thấy tệp out/main.wasm, is the output file after build.
Deploy the voting smart contract on NEAR Testnet blockchain
Before deploying, please go to the link https://wallet.testnet.near.org/ to create a wallet on Testnet. My wallet address on the Testnet is daothang.testnet
Please open Terminal, type the following command to login to the wallet:
near login
Then you follow the instructions.
Now we will use the above account to create a sub account and then deploy the smart contract:
// Create a sub account
near create-account voting1.daothang.testnet --masterAccount daothang.testnet --initialBalance 100
// Build và deploy smart contract to the testnet
yarn build:contract
near deploy --accountId voting1.daothang.testnet --wasmFile ./out/main.wasm
Next we will call the functions to test the smart contract.
We remember that our voting flow looks like this:
createVoting -> addCandidate() + vote() -> endVote()
// Create new voting
near call voting1.daothang.testnet createVoting '{}' --accountId daothang.testnet
// Add candidates for current voting
near call voting1.daothang.testnet addCandidate '{"candidate": "Donald Trump"}' --accountId daothang.testnet
near call voting1.daothang.testnet addCandidate '{"candidate": "Joe Biden"}' --accountId daothang.testnet
// Everyonce can vote
near call voting1.daothang.testnet vote '{"candidateId":0}' --deposit 0.1 --accountId <account1>
near call voting1.daothang.testnet vote '{"candidateId":0}' --deposit 0.1 --accountId <account2>
near call voting1.daothang.testnet vote '{"candidateId":1}' --deposit 0.1 --accountId <account3>
// End voting to latch result
near call voting1.daothang.testnet endVote '{}' --accountId daothang.testnet
In the above process we can check the voting information at any time by calling the following functions:
// View current voting / lastest voting
near view voting1.daothang.testnet votingInfo '{}'
// View any voting
near view voting1.daothang.testnet votingInfoById '{"votingId":0}'
Build the frontend for voting app
<Comming soon>
Trả lời