23
loading...
This website collects cookies to deliver better user experience
yarn
tasks to build the smart contract.startDate
and endDate
properties which are used to control the election process.startDate
.currentTime > election.startDate
.simple
and singleton
, lets define a new workspace item "src/voting"
in asconfig.json
so it will look like this:{
"workspaces": [
"src/simple",
"src/singleton",
"src/voting"
]
}
voting
and another two folders assembly
and __tests__
inside it:mkdir src/voting
mkdir src/voting/assembly
mkdir src/voting/__tests__
__tests__
will contain some unit tests, so we also need to have a file to support the testing library used, we can just copy it from another folder:cp -r src/simple/__tests__/as-pect.d.ts src/voting/__tests__/.
touch src/voting/__tests__/index.unit.spec.ts
index.unit.spec.ts
file:import { Contract } from "../assembly";
let contract: Contract
beforeEach(() => {
contract = new Contract()
})
describe("Voting Contract", () => {
// VIEW method tests
it("view method 1", () => {
// expect(contract.method_name()).toStrictEqual("expected_result")
})
// CHANGE method tests
it("change method 1", () => {
// expect(contract.methodName("some-key", "some value")).toStrictEqual("Data processed.")
})
})
src/voting/index.ts
:touch src/voting/assembly/index.ts
index.ts
file:@nearBindgen
export class Contract {
get_info(): string {
return "This is the near protocol voting app smart contract tutorial.";
}
}
yarn build:release
andbuild/release/
folder, to verify it contains our new contract:yarn
yarn build:release
ls build/release
build/release
folder including new voting.wasm
:➜ near-voting-app-smart-contract-tut git:(main) ✗ ls build/release/
simple.wasm singleton.wasm voting.wasm
model.ts
so it will hold all our models:touch src/voting/assembly/model.ts
model.ts
, copy and paste this code:import { PersistentMap, PersistentSet, u128 } from "near-sdk-as";
import { AccountId, Timestamp } from "../../utils";
@nearBindgen
export class Candidate {
constructor(
public accountId: AccountId,
public registrationDate: Timestamp,
public name: string,
public slogan: string,
public goals: string
) {}
}
@nearBindgen
export class Vote {
constructor(
public accountId: AccountId,
public date: Timestamp,
public candidateId: AccountId,
public comment: string,
public donation: u128
) {}
}
@nearBindgen
export class ElectionInfo {
constructor(
public id: u32,
public initiator: AccountId,
public creationDate: Timestamp,
public startDate: Timestamp,
public endDate: Timestamp,
public title: string,
public description: string
) {}
}
@nearBindgen
export class CandidateVotes {
constructor(public candidate: Candidate, public votes: Vote[]) {}
}
@nearBindgen
export class ElectionVotes {
constructor(public election: ElectionInfo, public votes: CandidateVotes[]) {}
}
@nearBindgen
export class Election {
public candidates: PersistentSet<Candidate>;
public candidateIds: PersistentSet<string>;
public votes: PersistentMap<AccountId, PersistentSet<Vote>>;
public voters: PersistentSet<AccountId>;
public electionInfo: ElectionInfo;
}
@nearBindgen
export class Election {
public candidates: PersistentSet<Candidate>;
public candidateIds: PersistentSet<AccountId>;
public votes: PersistentMap<AccountId, PersistentSet<Vote>>;
public voters: PersistentSet<AccountId>;
public electionInfo: ElectionInfo;
constructor(
id: u8,
initiator: AccountId,
creationDate: Timestamp,
startDate: Timestamp,
endDate: Timestamp,
title: string,
description: string
) {
this.electionInfo = new ElectionInfo(
id,
initiator,
creationDate,
startDate,
endDate,
title,
description
);
this.candidates = new PersistentSet<Candidate>(`e${id}_c`);
this.votes = new PersistentMap<AccountId, PersistentSet<Vote>>(`e${id}_v`);
this.candidateIds = new PersistentSet<string>(`e${id}_ci`);
this.voters = new PersistentSet<AccountId>(`e${id}_vt`);
}
}
PersistentSet
to store candidates
and candidateIds
.candidateIds
additionally? This is to be able to check if we have such candidate registered faster without the need of iterating over the objects.PersistenceMap
where the key is the candidate's account_id
it will help us to get votes for a specific candidate.ElectionInfo
class, it will be used to return the general election information later in the smart contract.voting/assembly/index.ts
so it will look like this:import { PersistentSet, PersistentMap } from "near-sdk-core";
import { Election } from "./model";
@nearBindgen
export class Contract {
private elections: PersistentMap<u32, Election>;
private electionIds: PersistentSet<u32>;
constructor() {
this.elections = new PersistentMap<u32, Election>("e");
this.electionIds = new PersistentSet<u32>("ei");
}
}
PersistentMap<u32, Election>
which will store all future elections,PersistentSet<u32>
of the electionId's so we can quickly check if the election with provided id exists.near view
. Let's add several handy methods. Copy those three code snippets in the contract:get_elections()
- get all existing elections:
get_elections(): ElectionInfo[] {
const electionIds = this.electionIds.values();
let elections: ElectionInfo[] = [];
for (let i: i32 = 0; i < electionIds.length; i++) {
elections.push(this.elections.getSome(electionIds[i]).electionInfo);
}
return elections;
}
get_candidates(electionId: u32)
- get all candidates for the specified electionId
:
get_candidates(electionId: u32): Candidate[] {
assert(
this.elections.contains(electionId),
`No election with id [${electionId}] found. Did you mistype?`
);
return this.elections.getSome(electionId).candidates.values();
}
get_votes(electionId: u32)
- get current voting results for the specified electionId
:
get_votes(electionId: u32): ElectionVotes {
assert(
this.elections.contains(electionId),
`No election with id [${electionId}] found. Did you mistype?`
);
const election = this.elections.getSome(electionId);
const allCandidates = election.candidates.values();
const candidatesVotes: CandidateVotes[] = [];
for (let i: i32 = 0; i < allCandidates.length; i++) {
const candidate = allCandidates[i];
let votes: Vote[];
if (election.votes.contains(candidate.accountId)) {
votes = election.votes.getSome(candidate.accountId).values();
} else {
votes = [];
}
const candidateVote = new CandidateVotes(candidate, votes);
candidatesVotes.push(candidateVote);
}
return new ElectionVotes(election.electionInfo, candidatesVotes);
}
import { PersistentSet, PersistentMap } from "near-sdk-core";
import {
Candidate,
CandidateVotes,
Election,
ElectionInfo,
ElectionVotes,
Vote,
} from "./model";
assert()
method in get_candidates
and get_votes
. This is very useful to add such assertions in your contracts, so you always know everything is going as expected:assert(this.elections.contains(electionId),`No election with id [${electionId}] found.`);
@mutateState()
annotation in the contract. Lets add 3 methods to the code:add_election
, add_candidacy
, and add_vote
.add_election(
title: string,
description: string,
startDate: Timestamp,
endDate: Timestamp
): void
title
, description
, startDate
and endDate
RNG
provided by near sdk.startDate
and endDate
will be used to set the start and end of election as a timestamp in milliseconds. You can pass 0
for both properties, then the default values will be used:@mutateState()
add_election(
title: string,
description: string,
startDate: Timestamp,
endDate: Timestamp
): void {
const rng = new RNG<u16>(1, u16.MAX_VALUE);
const electionId = rng.next();
const start = startDate > 0
? startDate * 1000000
: context.blockTimestamp + 86400000000000
const election = new Election(
electionId,
context.sender,
context.blockTimestamp,
startDate > 0
? startDate * 1000000
: context.blockTimestamp + 86400000000000,
endDate > 0
? endDate * 1000000
: start + 86400000000000 * 7,
title,
description
);
this.electionIds.add(electionId);
this.elections.set(electionId, election);
}
electionId
.@mutateState()
add_candidacy(
electionId: u32,
name: string,
slogan: string,
goals: string
): void {}
assert()
method to check if we have election with the specified id in the contract storage:assert(
this.elections.contains(electionId),
`No election with id [${electionId}] found. Did you mistype?`
);
assert()
to check if there is no such candidate in the current election:const election = this.elections.getSome(electionId);
assert(
election.electionInfo.startDate > context.blockTimestamp,
"Could not add candidacy to the ongoing elections."
);
name
, slogan
and goals
are defined. Because empty candidacy is no very informative:assert(
name.length > 0,
"Name is required, put your account ID as name if you was us to put it on the election billboard!"
);
assert(
slogan.length > 0,
"Slogan is required, what are you going to print on the snapbacks and t-shirts?"
);
assert(
goals.length > 0,
"Goals is required, who will vote to you without the goals?"
);
const date = context.blockTimestamp;
const candidate = new Candidate(candidateId, date, name, slogan, goals);
election.candidates.add(candidate);
election.candidateIds.add(candidateId);
this.elections.set(electionId, election);
@mutateState()
add_candidacy(
electionId: u32,
name: string,
slogan: string,
goals: string
): void {
const candidateId = context.sender;
assert(
this.elections.contains(electionId),
`No election with id [${electionId}] found. Did you mistype?`
);
const election = this.elections.getSome(electionId);
assert(
election.electionInfo.startDate > context.blockTimestamp,
"Could not add candidacy to the ongoing elections."
);
assert(
!election.candidateIds.has(candidateId),
"Candidate is already registered in this election, don't cheat! Your votes will not sum up in case you register yourself twice :)"
);
assert(
name.length > 0,
"Name is required, put your account ID as name if you was us to put it on the election billboard!"
);
assert(
slogan.length > 0,
"Slogan is required, what are you going to print on the snapbacks and t-shirts?"
);
assert(
goals.length > 0,
"Goals is required, who will vote to you without the goals?"
);
const date = context.blockTimestamp;
const candidate = new Candidate(candidateId, date, name, slogan, goals);
election.candidates.add(candidate);
election.candidateIds.add(candidateId);
this.elections.set(electionId, election);
}
add_vote
which allows users to submit their vote for a specific candidate in the election.@mutateState()
add_vote(electionId: u32, candidateId: string, comment: string): void {
assert(
this.elections.contains(electionId),
`No election with id [${electionId}] found. Did you mistype?`
);
const election = this.elections.getSome(electionId);
assert(
election.electionInfo.startDate > context.blockTimestamp,
"Could not add vote to the election which is not yet started."
);
assert(
election.electionInfo.endDate < context.blockTimestamp,
"Could not add vote to the election which is already finished."
);
const voterId = context.sender;
const date = context.blockTimestamp;
const donation = context.attachedDeposit;
assert(
election.candidateIds.has(candidateId),
"Candidate is not registered in the election. Maybe you mistyped his account id?"
);
assert(!election.voters.has(voterId), "Sorry, you can only vote once!");
election.voters.add(voterId);
const vote = new Vote(
voterId,
date,
candidateId,
comment ? comment : "",
donation
);
let votes = election.votes.get(candidateId);
if (votes == null) {
votes = new PersistentSet<Vote>("vt");
}
votes.add(vote);
election.votes.set(candidateId, votes);
this.elections.set(electionId, election);
}
near cli
using near login
command prior to this step:yarn build:release && near dev-deploy --wasmFile build/release/voting.wasm
➜ near-voting-app-smart-contract-tut git:(main) ✗ yarn build:release && near dev-deploy --wasmFile build/release/voting.wasm
yarn run v1.22.17
warning ../../package.json: No license field
$ asb
✨ Done in 16.10s.
Starting deployment. Account id: dev-1637852337467-70985280826279, node: https://rpc.testnet.near.org, helper: https://helper.testnet.near.org, file: build/release/voting.wasm
Transaction Id 641CmZv5cZHcWe773C8g5uaBDpqoTt8zp4CfBxjytEWR
To see the transaction in the transaction explorer, please open this url in your browser
https://explorer.testnet.near.org/transactions/641CmZv5cZHcWe773C8g5uaBDpqoTt8zp4CfBxjytEWR
Done deploying to dev-1637852337467-70985280826279
dev-1234567890-123456789
this is the contract name you will be using. It is also added to the file neardev/dev-account.env
and we will use is to set up ENV variables before we call the contract.new
method which will call the Contract
class constructor
. To init the contract use this command:source neardev/dev-account.env
near call $CONTRACT_NAME new --accountId $CONTRACT_NAME
near cli
using add_election
and then call get_elections
to check our new election. You can pass startDate to the current time stamp + 5 minutes, so you will have 5 minutes to submit the candidates before the voting start, you can use some service like currentmillis.com
source neardev/dev-account.env
near call $CONTRACT_NAME add_election '{"title": "First election!", "description": "Testing the election model.", "startDate": "1637874480000", "endDate": "0"}' --accountId $CONTRACT_NAME
near view $CONTRACT_NAME get_elections
get_election
will look like this:➜ near-voting-app-smart-contract-tutorial git:(main) ✗ near view $CONTRACT_NAME get_elections
View call: dev-1637871596730-46015068107726.get_elections()
[
{
id: 43094,
initiator: 'dev-1637871596730-46015068107726',
creationDate: '1637874334500905663',
startDate: '1637874360000000000',
endDate: '1638479160000000000',
title: 'First election!',
description: 'Testing the election model.'
}
]
38749
which you can use to submit candidacy, for you it will be different id, so you can use the once you received from the contract.add_candidacy
method and then you call get_candidates
to verify that your candidacy has been added:source neardev/dev-account.env
near call $CONTRACT_NAME add_candidacy '{"electionId": 38749, "name": "Donald Duck", "slogan": "Make river great again!", "goals": "Do good, do not do bad!"}' --accountId $CONTRACT_NAME
near view $CONTRACT_NAME get_candidates '{"electionId": 38749}'
➜ near-voting-app-smart-contract-tutorial git:(main) ✗ near view $CONTRACT_NAME get_candidates '{"electionId": 42719}'
View call: dev-1637871596730-46015068107726.get_candidates({"electionId": 38749})
[
{
accountId: 'dev-1637871596730-46015068107726',
registrationDate: '1637873336622836513',
name: 'Donald Duck',
slogan: 'Make river great again!',
goals: 'Do good, do not do bad!'
}
]
add_vote
.#!/bin/bash
source neardev/dev-account.env
near call $CONTRACT_NAME add_vote '{"electionId": 38749, "candidateId": "dev-1637871596730-46015068107726", "comment": "I believe that guy!"}' --accountId $CONTRACT_NAME
get_votes
to check the current votes of the election:#!/bin/bash
source neardev/dev-account.env
near view $CONTRACT_NAME get_votes '{"electionId": 38749}'
➜ near-voting-app-smart-contract-tutorial git:(main) ✗ near view $CONTRACT_NAME get_votes '{"electionId": 38749}'
View call: dev-1637871596730-46015068107726.get_votes({"electionId": 38749})
{
election: {
id: 38749,
initiator: 'dev-1637871596730-46015068107726',
creationDate: '1637874426134027892',
startDate: '1637874480000000000',
endDate: '1638479280000000000',
title: 'First election!',
description: 'Testing the election model.'
},
votes: [
{
candidate: {
accountId: 'dev-1637871596730-46015068107726',
registrationDate: '1637874452572363281',
name: 'Donald Duck',
slogan: 'Make river great again!',
goals: 'Do good, do not do bad!'
},
votes: [
{
accountId: 'dev-1637871596730-46015068107726',
date: '1637874808410611274',
candidateId: 'dev-1637871596730-46015068107726',
comment: 'I believe that guy!',
donation: '0'
}
]
}
]
}
endDate
will be less than the current date it will mean that election process is finished and you will not be able submit votes, so get_votes
will be giving the final results of voting.near deploy --wasmFile build/release/voting.wasm --accountId voting.your_account.testnet
➜ near-voting-app-smart-contract-tutorial git:(main) ✗ near create-account voting.your_account.testnet --masterAccount lkskrnk.testnet
Saving key to '/Users/oleksandrkorniienko/.near-credentials/testnet/voting.your_account.testnet.json'
Account voting.your_account.testnet for network "testnet" was created.