## Voting System Tutorial ### Create Objects/Define Data #### Create the blockchain <pre> ignite scaffold chain <b>votingsystem</b> --no-module cd votingsystem </pre> #### Create Module <pre> ignite scaffold module <b>votingsystem</b> --dep bank </pre> #### Create System Variables - Election Counter - Tokens Number Issued - Max Tokens <pre> ignite scaffold single systemInfo electionCounter:uint tokensAllocated:uint tokensMax:uint --module votingsystem --no-message </pre> Edit proto/votingsystem/genesis.proto <pre> SystemInfo systemInfo = 2 <b>[(gogoproto.nullable) = false];</b> </pre> Run <pre> ignite generate proto-go </pre> Edit x/votingsystem/types/genesis.go **From:** <pre> func DefaultGenesis() *GenesisState { return &GenesisState{ SystemInfo: nil, </pre> **To** <pre> const DefaultIndex uint64 = 1 const NumberOfTokens uint64 = 0 const MaxTokensToIssue uint64 = 1 func DefaultGenesis() *GenesisState { return &GenesisState{ SystemInfo: SystemInfo{ ElectionCounter: uint64(DefaultIndex), TokensAllocated: uint64(NumberOfTokens), TokensMax: uint64(MaxTokensToIssue), }, }, </pre> Edit x/votingsystem/genesis.go **From** <pre> if genState.SystemInfo != nil { k.SetSystemInfo(ctx, *genState.SystemInfo) } </pre> **To** <pre> if genState.SystemInfo != (types.SystemInfo{}) { k.SetSystemInfo(ctx, genState.SystemInfo) } </pre> Also (around line 40) **From** <pre> if found { genesis.SystemInfo = &systemInfo } </pre> **To** <pre> if found { genesis.SystemInfo = systemInfo } </pre> ### Create Objects/Define Data We need to define the structures that will store the data of the voting system - Election  *(A thing for people to vote on. Only yes/no votes.)* - Vote *(A token that allows a vote. Transferrable.)* - Ballot *(A ballot that is tied to an election and must be cast by token holder.)* #### Commands to create data structures <pre> ignite scaffold map vote owner nickname --module votingsystem --no-message ignite scaffold map election desc voter startDate endDate --module votingsystem --no-message ignite scaffold map ballot electionid:uint vote choice:bool --module votingsystem --no-message </pre> #### Commands to create data messages <pre> ignite scaffold message createElection desc startDate endDate --module votingsystem --response electionId ignite scaffold message castBallot electionId:uint voteToken choice:bool --module votingsystem ignite scaffold message issueVoteToken nickname --module votingsystem ignite scaffold message transferVoteToken tokenIndex newTokenHolder nickname --module votingsystem </pre> #### Commands to create queries <pre> ignite scaffold query electionResult electionID --module votingsystem --response votesFor,votesAgainst ignite scaffold query myVoteList --module votingsystem --response voteList </pre> - **Make method to create election** Edit election file x/votingsystem/keeper/msg\_server\_create\_election.go <pre> ctx := sdk.UnwrapSDKContext(goCtx) // Get and increment election counter systemInfo, found := k.Keeper.GetSystemInfo(ctx) if !found { panic("SystemInfo not found") } nextIndex := systemInfo.ElectionCounter nextIndex++ newIndex := strconv.FormatUint(nextIndex, 10) // Get vote info // Check dates if false { return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "Bid is not high enough") } startDate := msg.StartDate endDate := msg.EndDate thisElection := types.Election{ Index: newIndex, Desc: msg.Desc, Voter: msg.Creator, StartDate: startDate, EndDate: endDate, } k.SetElection(ctx, thisElection); // Save the systemInfo number of tokens systemInfo.ElectionCounter = nextIndex; k.SetSystemInfo(ctx, systemInfo) return &types.MsgCreateElectionResponse{ElectionId: newIndex}, nil </pre> Edit election file x/votingsystem/keeper/msg\_server\_cast\_ballot.go <pre> ctx := sdk.UnwrapSDKContext(goCtx) // Try getting a election from the store ElectionId, electionValid := k.GetElection(ctx, strconv.FormatUint(uint64(msg.ElectionID), 10)) voteToken, voteValid := k.GetVote(ctx, msg.VoteToken) if !electionValid || !voteValid { // Make custom error. This is just temporary return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "You've provided bad data") } if msg.Creator != msg.VoteToken { // Make custom error. This is just temporary return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "You've provided bad data") } externalAnswer, err := callExternalService(ElectionId.String()) if err != nil { log.Fatalf("did not connect: %v", err) } fmt.Println("From External Service: " + externalAnswer) thisBallot := types.Ballot{ Index: ElectionId.String(), Vote: voteToken.String(), Choice: msg.Choice, } k.SetBallot(ctx, thisBallot); return &types.MsgCastBallotResponse{}, nil </pre> Edit election file x/votingsystem/keeper/msg\_server\_issue\_vote\_token.go <pre> // In the import section "strconv" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" // In the IssueVoteToken func ctx := sdk.UnwrapSDKContext(goCtx) systemInfo, found := k.Keeper.GetSystemInfo(ctx) if !found { panic("SystemInfo not found") } nextIndex := systemInfo.TokensAllocated nextIndex++ newTokenIndex := strconv.FormatUint(nextIndex, 10) if nextIndex > systemInfo.TokensMax { return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "All vote tokens have been issued") } newVote := types.Vote{ Index: newTokenIndex, Owner: msg.Creator, Nickname: msg.Nickname, } k.SetVote(ctx, newVote) // Save the systemInfo number of tokens systemInfo.TokensAllocated = nextIndex; k.SetSystemInfo(ctx, systemInfo) return &types.MsgIssueVoteTokenResponse{}, nil </pre> Edit election file x/votingsystem/keeper/msg\_server\_transfer\_vote\_token.go <pre> // In import sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" // In TransferVoteToken func ctx := sdk.UnwrapSDKContext(goCtx) // If the message didn't come from the token holder, error out voteToken, voteValid := k.GetVote(ctx, msg.TokenIndex) if !voteValid { return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "Token not found") } if msg.Creator != voteToken.Owner { return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "Non-Owner tried to tranfer token") } newVote := types.Vote{ Index: voteToken.Index, Owner: msg.NewTokenHolder, Nickname: msg.Nickname, } k.SetVote(ctx, newVote) return &types.MsgTransferVoteTokenResponse{}, nil </pre> #### Add Error Messages Add some error messages (x/votingsystem/types/errors.go) <pre> ErrInvalidDeadline = sdkerrors.Register(ModuleName, 1109, "Deadline cannot be parsed: %s") </pre> #### Add Unit Tests Make a list of test files and sample content <pre> Sample test code </pre> Add external program call to ballot casting Edit election file x/votingsystem/keeper/msg\_server\_cast\_ballot.go **Add** <pre> // At top, in import section "strconv" "flag" "log" "time" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" // Inside CastBallot Method, before "thisBallot := types.Ballot" externalAnswer, err := callExternalService(ElectionId.String()) if err != nil { log.Fatalf("did not connect: %v", err) } fmt.Println("From external service: " + externalAnswer) // At bottom func callExternalService(dataToSend string) (string, error) { var ( addr = flag.String("addr", "localhost:50051", "the address to connect to") name = flag.String("name", dataToSend, "Name to greet") ) flag.Parse() // Set up a connection to the server. conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := types.NewGreeterClient(conn) // Contact the server and print out its response. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.SayHello(ctx, &types.HelloRequest{Name: *name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage()) return r.GetMessage(), nil } </pre> Create proto file proto/votingsystem/greeter.proto **Add** <pre> syntax = "proto3"; package votingsystem.votingsystem; option go_package = "votingsystem/x/votingsystem/types"; service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } </pre> ## Set up the queries Edit the file proto/votingsystem/query.proto **Change** <pre> message QueryMyVoteListResponse { string voteList = 1; } </pre> **To** <pre> message QueryMyVoteListResponse { repeated string vote = 1 [(gogoproto.nullable) = false]; cosmos.base.query.v1beta1.PageResponse pagination = 2; } </pre> Run command <pre> ignite generate proto-go </pre> Edit file x/votingsystem/keeper/grpc\_query\_my\_vote\_list.go <pre> // Body of MyVotingList, after ctx line // Define a variable that will store a list of posts var votes []*types.Vote // Get the key-value module store using the store key (in this case store key is "chain") store := ctx.KVStore(k.storeKey) // Get the part of the store that keeps posts (using vote key, which is "Vote-value-") voteStore := prefix.NewStore(store, []byte(types.VoteKey)) pageRes, err := query.Paginate(voteStore, req.Pagination, func(key []byte, value []byte) error { var vote types.Vote if err := k.cdc.Unmarshal(value, &vote); err != nil { return err } votes = append(votes, &vote) return nil }) return &types.QueryMyVoteListResponse{Vote: votes}, nil </pre> Edit file x/votingsystem/keeper/grpc\_query\_election\_result.go <pre> // Add to import section "strconv" // Body of ElectionResult, after ctx line // Define a variable that will store a list of posts var ballotFor int = 0 var ballotAgainst int = 0 // Get the key-value module store using the store key (in this case store key is "chain") store := ctx.KVStore(k.storeKey) // Get the part of the store that keeps posts (using ballot key, which is "Ballot-value-") ballotStore := prefix.NewStore(ballot, []byte(types.BallotKey)) // Terrible way to do this. Need to find way to just query what I want instead of everything pageRes, err := query.Paginate(ballotStore, req.Pagination, func(key []byte, value []byte) error { var ballot types.Ballot if err := k.cdc.Unmarshal(value, &vote); err != nil { return err } if ballot.ElectionId == msg.ElectionId { if ballot.Choice == "true" { ballotFor++ } else { ballotAgainst++ } } return nil }) return &types.QueryElectionResultResponse{VotesFor: strconv.Itoa(ballotFor), VotesAgainst: strconv.Itoa(ballotAgainst)}, nil </pre> Start up your new blockchain(zone) <pre> ignite chain serve </pre>