LapTrinhBlockchain

Chia sẻ kiến thức về Lập Trình Blockchain

Lập trình Blockchain, Lập trình Smart Contract

How to writing a voting app (Smart contract + Frontend) on NEAR

How to writing a voting app (Smart contract + Frontend) on NEAR

How to writing a voting app (Smart contract + Frontend) on NEAR

Chia sẻ bài viết
5
(2)

Today, I will guide to write voting app on NEAR blockchain. The tutorial will consist of three parts:

  1. 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)
  2. How to deploy the voting smart contract to NEAR Testnet
  3. How to build the frontend to create complete voting app

Now let’s start!

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:

Project folder structure
Project folder structure

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.

Folder structure in contract folder
Folder structure in contract folder

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.

Build the voting smart contract
Build the voting smart contract

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}'
The information of a voting
The information of a voting

Build the frontend for voting app

<Comming soon>

Bài viết này có hữu ích với bạn?

Kích vào một biểu tượng ngôi sao để đánh giá bài viết!

Xếp hạng trung bình 5 / 5. Số phiếu: 2

Bài viết chưa có đánh giá! Hãy là người đầu tiên đánh giá bài viết này.

Trả lời

Giao diện bởi Anders Norén