Build Voting Smart Contract On Stratis Blockchain

Introduction

In this article, you will learn how to build a real-world voting smart contract with C# and Stratis. In the first article of this series, we learned about the tools and configurations required to build your first smart contract over Stratis blockchain. So, if you haven’t read that article yet, please walk through it first: Write Your First Smart Contract on Stratis

Prerequisites

Here are the prerequisites to complete this tutorial,

  • Visual Studio 2019 Community or later version
  • Stratis Smart Contract Template
  • Sct tool
  • Stratis FullNode
  • Postman
  • Swagger

Define Contract Behaviours

The voting system is a complex subject, and covering all aspects is quite difficult. Our goal is to showcase the feature and implementation of a smart contract. Hence, we’ll build our contract with consideration of limited scope. However, feel free to extend this contract with points given in the exercise section at the end of this article.

We will create a Smart Contract called “Ballot”. It should accept a list of proposals—Proposals could be anything like candidates or survey options—during deployment. The wallet address from the contract will be deployed, will become a chairperson. The chairperson can give a right to others' wallet addresses (users) to vote. A voter—with a voting right—can invoke the method “Vote” and register their vote. The proposal that received the highest vote should be considered as the winning proposal. Anyone can call the method to get the winning proposal and check which one is the winning proposal.

Flowchart

To better understand the smart contract, you can go through the following,

GiveRightToVote()

Vote()

With these considerations, let's build the smart contract.

Create Sample Contract project

Create a new project with the Stratis template. I’ve created with the name “VotingContract”. Initially, the project solution looks like the below screen.

The first line in the contract is a reference to the Stratis.SmartContracts NuGet package. This package allows you to inherit from the “SmartContract” class. And thereby we can use useful functionality like sending funds, hashing, and saving data.

Next, change the targeted framework to 3.1. To do that, go to project properties > Applications > Find Target framework dropdown. This will help us to avoid packages version issues later while development.

Modify the contract name and give a meaningful name. I’m using the name “Ballot” for the contract.

Contract Development

Now we need a "Structure" to represent a single voter. It stores the information of a voter like voting weight, a voter is voted or not and the proposal index on voter is voted. So, let’s add one struct called “Voter”.

public struct Voter
{
    /// <summary>
    /// Voting weight of the voter.
    /// </summary>
    public uint Weight;

    /// <summary>
    /// Is the voter voted
    /// </summary>
    public bool Voted;

    /// <summary>
    /// Proposal index of the vote.
    /// </summary>
    public uint VoteProposalIndex;
}

We need a proposal structure to hold the information of proposals and voting count of each. So, take one more structure "Proposal".

public struct Proposal
{
 /// <summary>
 /// Name of the proposal.
 /// </summary>
 public string Name;

 /// <summary>
 /// Total vote received for the proposal.
 /// </summary>
 public uint VoteCount;
}

We also need to have Get and Set methods for both the structures we defined.  To persist data into the smart contract, use PersistentState. The PersistentState stores data on a key, and we need to use the same key to retrieve data. To read more about PersistentState, visit.

Here, I have used the user wallet address with a string combination (voter:walletaddress) to store the voter structure information. 

private Voter GetVoter(Address address) => PersistentState.GetStruct<Voter>($"voter:{address}");
private void SetVoter(Address address, Voter voter) => PersistentState.SetStruct($"voter:{address}", voter);

Note:

There are mainly two access modifiers are used in the smart contract—Public and Private. When you make anything public, that means anyone can call it from the outside world. If you make it private, only the smart contract class can access it. 

The proposals should be stored only once when the contract is deployed. And we are not sure how many proposals anyone will be going to add; hence, we use a dynamic array to store proposals.

public Proposal[] Proposals
{
    get => PersistentState.GetArray < Proposal > (nameof(Proposals));
    private set => PersistentState.SetArray(nameof(Proposals), value);
}

Now let’s define the contract creator as chairperson. To store that field, we are going to add one new wallet-address property.

public Address ChairPerson 
{
    get => PersistentState.GetAddress(nameof(ChairPerson));
    private set => PersistentState.SetAddress(nameof(ChairPerson), value);
}

Store the value of the chairperson's property while the deployment of the contract.

public class Ballot: SmartContract 
{
    public Ballot(ISmartContractState smartContractState): base(smartContractState) 
	{
		ChairPerson = Message.Sender;
    }
        .....
}

We will pass a list of proposals while deploying the contract. So, that needs to be stored as well. So, let’s take the byte array as a parameter in the constructor, and then serialize it to an array of the proposal. At the time of writing this article, it is not allowed to pass struct array as a parameter to the Smart Contract directly.

public class Ballot: SmartContract 
{
    public Ballot(ISmartContractState smartContractState, byte[] proposals): base(smartContractState)
	{
		ChairPerson = Message.Sender;
		var proposalsArray = Serializer.ToArray < Proposal > (proposals);
    }
    .....
}

Whenever we take input from the user, it is better to validate that before doing any operation. We can achieve this using the helper method. So, let’s take one helper method— ValidatePropsalAndAssign—to validate the length of the proposals array. If the length of an array is correct, assign the proposal array to the “Proposal” property.

private void ValidateProposalAndAssign(Proposal[] proposals) 
{
    Assert(proposals.Length > 1, "Please provide at least 2 proposals");
    this.Proposals = proposals;
}

Note:

Assert is the method of the “SmartContract” class. It is used for validation.

Syntax:

Assert(bool condition, string message = "Assert failed.");

Simply, if the condition is not met, it will throw an exception and the code execution won’t go further. In our case, if the proposal's array length is not greater than 1, it will throw an error. You can add a message to identify the issue while executing the smart contract, otherwise, sometimes a single method can have multiple validations and with a default message, it would be difficult to identify which validation is throwing an error.

Now, let’s pass chairperson values from the contractor.

public Ballot(ISmartContractState smartContractState, byte[] proposals): base(smartContractState) 
{
    ChairPerson = Message.Sender;
    var proposalsArray = Serializer.ToArray < Proposal > (proposals);
    ValidateProposalAndAssign(proposalsArray);
}

We have set the proposal's value and chairperson wallet address. Now, let’s add a method to give a voting right to the user. Remember, only the chairperson can call this method.

public bool GiveRightToVote(Address voterAddress) {
    Assert(Message.Sender == ChairPerson, "Only chairperson can give right to vote.");
    var voter = this.GetVoter(voterAddress);
    Assert(voter.Weight == 0, "The voter already have voting rights.");
    Assert(!voter.Voted, "Already voted.");
    voter.Weight = 1;
    this.SetVoter(voterAddress, voter);
    return true;
}

We have set the updated voter information to the PersistentState, so whenever we will retrieve the voter information using the same key, it should give the updated state information.

Further, add one method to vote. A voter will pass the proposal index and the method captures the vote. Also, it should update the proposal array and increase the voting count.

public bool Vote(uint proposalId) {
    var voter = this.GetVoter(Message.Sender);
    Assert(voter.Weight == 1, "Has no right to vote.");
    Assert(!voter.Voted, "Already voted.");
    voter.Voted = true;
    voter.VoteProposalIndex = proposalId;
    Proposals[proposalId].VoteCount += voter.Weight;
    this.SetVoter(Message.Sender, voter);
    Log(new Voter {
        Voted = true,
            Weight = 1,
            VoteProposalIndex = proposalId
    });
    return true;
}

Every transaction has some fee associated and reading data for each record would not be the best way. Instead, the Log method of the SmartContract class enables you to log the data that can be queried using API.  

Add one more method that counts the number of votes and return the winning proposal.

public uint WinningProposal() 
{
    uint winningVoteCount = 0;
    uint winningProposalId = 0;

    for (uint i = 0; i < Proposals.Length; i++) 
	{
        if (Proposals[i].VoteCount > winningVoteCount) 
		{
            winningVoteCount = Proposals[i].VoteCount;
            winningProposalId = i;
        }
    }

    return winningProposalId;
}

Similarly, to get the winning proposal name, we add one more method, which will use the method “WinningProposal” and retrieve the name of the proposal.

public string WinnerName() 
{
    var winningProposalId = WinningProposal();
    var proposals = Proposals[winningProposalId];
    return proposals.Name;
}

The contract implementation is done. Next, we will understand Parameter serialization.

Parameter Serialization

As per the Stratis docs, the contract parameters must be provided as a string. This requires that a parameter is serialized to a string in the format that the API is expecting. Additionally, when using the API or SCT, the type of each parameter must be provided in the format “{0}#{1}”, where: {0} is an integer representing the Type of the serialized data and {1} is the serialized data itself.

Refer to this table to see the mapping between a type and its integer representation, the serializer for the type, and an example of using the type as a parameter.

So, if you want to pass a list of proposals to the contract, we need to pass in the format like

parameters: [
 "10#Hex_String"
]

But we don't have the hex string value that the contract expects. So, let's get the hex string. The simplest way to create a hex string is to add a test case for it.

Add Tests Project

Add a new class library to the voting contract project.

Choose xUint Test Project:

Provide Project name,

Choose targeted framework .NET Core 3.1.

Next, Install require NuGet packages for test cases. Go to Tools > NuGet Package Manager > Package Manager console.

Install the following package. Make sure to select the Test project.

Install-Package Moq -Version 4.13.1
Install-Package Stratis.SmartContracts.CLR -Version 2.0.1

We also need to add a reference to the Smart Contract library project.

Rename the class name from UnitTest1 to BallotTests and add modify the code as below.

namespace VotingContract.Tests 
{
    using Moq;
    using NBitcoin;
    using Stratis.SmartContracts.CLR.Serialization;
    using Stratis.SmartContracts.Core;
    using Xunit;
    using Xunit.Abstractions;
    using Proposal = Ballot.Proposal;

    public class BallotTests 
	{
        private readonly ITestOutputHelper testOutputHelper;
        private Serializer serializer;
        private Mock < Network > network;

        public BallotTests(ITestOutputHelper testOutputHelper) 
		{
            this.testOutputHelper = testOutputHelper;
            this.network = new Mock < Network > ();
            this.serializer = new Serializer(new ContractPrimitiveSerializer(this.network.Object));
        }

        [Fact]
        public void SerializePraposalAsHexString() 
		{
            var proposals = new [] {
                new Proposal { Name = "Joe Biden", VoteCount = 0 },
                new Proposal { Name = "Donald Trump", VoteCount = 0 }
            };

            this.testOutputHelper.WriteLine(this.serializer.Serialize(proposals).ToHexString());
        }
    }
}

Here, we created a proposal array with two records, serialized it, and created a hex string. If you don't understand this code at this point, don't worry! we'll understand more about test cases in some other article.

Run the test by right click and select the "Run Test(s)" options.

Then, go to text explorer and open the test summary.

Then, click on "Open additional output for this result" You should see the output. That we need to pass in parameters while deploying the Smart Contract like:

parameters: [
 "10#E590CF894A6F6520426964656E840000000093D28C446F6E616C64205472756D708400000000"
]

Validation and Compilation

Next, we’ll validate and compile our contract using the Sct tool.

If the contract is valid, you’ll get the bytecode of the contract, that will be used while deploying the contract.

So, go to the sct project directory and run the below commands.

cd src/Stratis.SmartContracts.Tools.Sct
dotnet run -- validate [CONTRACT_PATH_HERE] -sb

Deployment

 

Wallet Load

Pass the credentials of the pre-defined wallet in the load wallet API /api/Wallet/load.

Parameters Value
Name cirrusdev
Password password

This API is to make sure that the private chain is running properly, and we are ready to go to the next step.

Get Wallet Addresses to Test the Contract

The contract requires some wallet addresses for chairperson and voters, and the wallet address we use should require some balance to execute the transaction.

So, to grab those addresses, let’s hit the API endpoint: /api/Wallet/balance. it will return the wallet address along with the balance of each.

 

Sample response:

{
    "balances": [
    {
        "accountName": "account 0",
        "accountHdPath": "m/44'/400'/0'",
        "coinType": 400,
        "amountConfirmed": 100000000000000,
        "amountUnconfirmed": 0,
        "spendableAmount": 100000000000000,
        "addresses": [
        {
            "address": "PFZhoVY7AE5TkRNKEcuRNR8CvZZtnP2Dpq",
            "isUsed": true,
            "isChange": false,
            "amountConfirmed": 18708420,
            "amountUnconfirmed": 0
        },
        {
            "address": "PEpYnf7Zv5u4pTDGmvYPT2vktpop2VeCqj",
            "isUsed": true,
            "isChange": false,
            "amountConfirmed": 999987198400,
            "amountUnconfirmed": 0
        },
        {
            "address": "PQ7CzignhCH8YSsZLtvT6V56PY3tfXSKSd",
            "isUsed": true,
            "isChange": false,
            "amountConfirmed": 99997644500,
            "amountUnconfirmed": 0
        },
        {
            "address": "PJs85PCxHddAXf4WSuG8MoPEGxSmEedTjG",
            "isUsed": true,
            "isChange": false,
            "amountConfirmed": 1000000000000,
            "amountUnconfirmed": 0
        },
        {
            "address": "PKuy4dacz3JYnEJiB43e1pNVHdHJscehEs",
            "isUsed": true,
            "isChange": false,
            "amountConfirmed": 9999996497600,
            "amountUnconfirmed": 0
        },
        {
            "address": "P95hHtFFtjvoKYFoFWAWPa1mmd48eqTVrc",
            "isUsed": false,
            "isChange": false,
            "amountConfirmed": 0,
            "amountUnconfirmed": 0
        },
        {
            "address": "PS64rEtXFtdQAnoKma5SLURrXsPo5bxg1r",
            "isUsed": false,
            "isChange": false,
            "amountConfirmed": 0,
            "amountUnconfirmed": 0
        },
        {
            "address": "PRfgPzU3sKDAoTuWsGFFh23oCXUYWmEd7b",
            "isUsed": false,
            "isChange": false,
            "amountConfirmed": 0,
            "amountUnconfirmed": 0
        },
        {
            "address": "P8d62MR1w3214tVZ8MYrbMxQXpb3rXfnAZ",
            "isUsed": false,
            "isChange": false,
            "amountConfirmed": 0,
            "amountUnconfirmed": 0
        },
        {
            "address": "PB1g8GxcPjPPwk3jedefQtSXqaWLtLyjsq",
            "isUsed": false,
            "isChange": false,
            "amountConfirmed": 0,
            "amountUnconfirmed": 0
        },
        {
            "address": "PPNohi19SKEScs6ggZdVGVDK5P34SHgYnC",
            "isUsed": false,
            "isChange": false,
            "amountConfirmed": 0,
            "amountUnconfirmed": 0
        },
        {
            "address": "PWLSVAhdgxt1oU4V9nUn8SeEGdV9xiuCT8",
            "isUsed": false,
            "isChange": false,
            "amountConfirmed": 0,
            "amountUnconfirmed": 0
        },
        {
            "address": "PBgFJdfdLMrscXEo6DA5heFZ3MLoTiBFCd",
            "isUsed": false,
            "isChange": false,
            "amountConfirmed": 0,
            "amountUnconfirmed": 0
        }]
    }]
}

As you can see there are some wallet addresses which has balance, we can use those. We’ll consider below wallet addresses:

Wallet Address Role
PEpYnf7Zv5u4pTDGmvYPT2vktpop2VeCqj Chairperson
PQ7CzignhCH8YSsZLtvT6V56PY3tfXSKSd Voter1
PFZhoVY7AE5TkRNKEcuRNR8CvZZtnP2Dpq Voter2
PKuy4dacz3JYnEJiB43e1pNVHdHJscehEs Voter3

If you don’t have a balance in other wallet addresses, send it from the default address balance using below api endpoint /api/Wallet/splitcoins. Refer to this if you want to know how to split coins, or you can ask about it in the comment section.

Deploy The Contract

A contract can be deployed using this endpoint. Here we have to pass bytecodes generated using the sct tool previously. Also, we’ll pass the parameters as we prepared during serializing.

Parameter Value Description
Amount 0 STRAX amount sends to the contract.
contractCode   Byte code generated using sct tool
password password Wallet password
sender PEpYnf7Zv5u4pTDGmvYPT2vktpop2VeCqj Sender wallet address
walletName cirrusdev Wallet name
accountName account 0 Account name
outpoints   null
feeAmount 0.001 Fee amount
gasPrice 100 Gas price
gasLimit 100000 Gas Limit
parameters [ "10#E590CF894A6F6520426964656E840000000093D28C446 F6E616C64205472756D708400000000" ] Parameters to pass to the contract method.

This API will return the transaction hash like "68f8649087bcac8a33330c7c13fb37108059884c34ebb090e99d9f567e4c0e44"”

Get Transaction Receipt

Using transaction hash, you can get transaction receipt which includes Gas Used in the transaction, From and To Addresses, Return Values, Logs, etc.

Find receipt API from the Smart Contracts folder in the collection. Provide transaction hash in the query parameter.

We will use this often to get the result of all transactions we will do on the blockchain like give voting right or vote on the proposal.

Sample response:

{
    "transactionHash": "68f8649087bcac8a33330c7c13fb37108059884c34ebb090e99d9f567e4c0e44",
    "blockHash": "58f02d9fffd00540072c8e93beda1ae3812ebfe2d7101900ad2c6c354555da7d",
    "postState": "e58b4267d756f8044762f6c920350d46e734a6b54fa664d6525a1fefb86962e5",
    "gasUsed": 13651,
    "from": "PEpYnf7Zv5u4pTDGmvYPT2vktpop2VeCqj",
    "to": null,
    "newContractAddress": "PWTQCDnvXR37TzpppSW4FVJQywV3F1M9nF",
    "success": true,
    "returnValue": null,
    "bloom": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000",
    "error": null,
    "logs": []
}

Please note down this contract address, it will use it while interacting with the smart contract deployed.

Give Voting Right

As we saw in the previous article, there are two ways to interact with a smart contract. If want to make changes to the smart contract state, we need to use a contract call, otherwise use a local call to read the data.

So, here we're going to use contract call to provide a voting right to a voter wallet address.  Remember, the only chairperson can give a voting right to the other wallet address.

Let’s call the smart contract method with call endpoint.

Parameters

Parameter Value Description
Amount 0 STRAX amount sends to the contract.
contractAddress PWTQCDnvXR37TzpppSW4FVJQywV3F1M9nF Contract address
methodName GiveRightToVote Contract method name
password password Wallet password
sender PEpYnf7Zv5u4pTDGmvYPT2vktpop2VeCqj Sender wallet address
walletName cirrusdev Wallet name
accountName account 0 Account name
outpoints   null
feeAmount 0.001 Fee amount
gasPrice 100 Gas price
gasLimit 100000 Gas Limit
parameters [ "9#PQ7CzignhCH8YSsZLtvT6V56PY3tfXSKSd" ] Parameters to pass to the contract method.
“9#{voter1_walletaddress}”

Sample Output

{
    "fee": 10100000,
    "hex": "0100000002440e4c7e569f9de990b0eb344c8859801037fb137c0c33338aacbc879064f868000000006b483045022100b79
03e456aa2da59f9bf1650caeb5e063d754be21034e710fad86f088c362d0102200f38275b1c8e6ff2008d2c7d711abf0bb14
a1a644f088b02cc36c62942b83845012103531e23f7f3c42782d72cb5e004d756a0364ed96e72b9d5febd0d3b9e142667b3f
ffffffffcbf8e4c121e0d03ca377176e4a8a189dfb04419a0119e4d6dd791fc691e3b7b010000006b48304502210092dd470
926dc2059c25ea7b1a91058d7ea45a7434374b786968edf5b48ef456902205782ef9884cf953b82536c51724e59fa1ff64cb
0bb6063e31a237b58f3d317b2012103531e23f7f3c42782d72cb5e004d756a0364ed96e72b9d5febd0d3b9e142667b3fffff
fff02f4885a00000000001976a91444583858cb5014d95c5f3df978fe04bab6e22ffd88ac000000000000000052c10100000
06400000000000000a086010000000000efda52e1ec42fb18f855c7f4b4f14d4974225231e88f476976655269676874546f5
66f746597d69509aa37e6e5e1086acb8c8faf4428a22fcabfa68ccb00000000",
    "message": "Your CALL method GiveRightToVote transaction was successfully built.",
    "success": true,
    "transactionId": "b5ed91ca325d9e204e73e965072b7423d642d8602cadb5b523cf93f27426b913"
}

Here we get the transaction ID in return. Using this call we can’t be sure whether our method was executed properly or not. To get that result we need to call the endpoint for transaction receipt. So, copy the transaction ID and pass it to the receipt endpoint.

Sample response

{
 "transactionHash": "b5ed91ca325d9e204e73e965072b7423d642d8602cadb5b523cf93f27426b913",
 "blockHash": "e978a963a1e5f170d6db14812d5bb1bd813e9bbeaf6e5e50499dbdc6ef1d3594",
 "postState": "5386bceb856e00d1efcc64c3db8e31433bcc73a650018222d87746e617d15c3d",
 "gasUsed": 11365,
 "from": "PEpYnf7Zv5u4pTDGmvYPT2vktpop2VeCqj",
 "to": "PWTQCDnvXR37TzpppSW4FVJQywV3F1M9nF",
 "newContractAddress": null,
 "success": true,
 "returnValue": "True",
 "bloom": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000",
 "error": null,
 "logs": []
}

Few things to notice here,

  • The transaction is from the chairperson and we’re executing the smart contract. So, “from” is the wallet address of the chairperson, and “To” is the smart contract address.
  • The value of the parameter “success” is true, meaning that this transaction has been executed without any error.
  •  “returnValue” is the value we are returning from the smart contract method. If you see the code, we have return “True”.
  • “error” returns the errors/validation errors.
  • “logs” return logs if we have added any in the method.

We have added validation to this method like if the sender is not the chairperson, the method should return the message. So, let’s check that validation is working properly or not.

Check Validation

Execute endpoint to call the method, but this time make voter1(i.e., PQ7CzignhCH8YSsZLtvT6V56PY3tfXSKSd) as sender. You will get the transaction id. Obtain that ID and get transaction receipt, you should see the success param value false and the message we set in the validation.

Sample response

{
    "transactionHash": "b3c72083d467df9708e994e73ba6723706b8f2f95cfb2d9784308ae26bca7a41",
    "blockHash": "f9e1e37ce68e1a830121d8236da9ccd6a876ca8b37a5502faba31e903f988d84",
    "postState": "5386bceb856e00d1efcc64c3db8e31433bcc73a650018222d87746e617d15c3d",
    "gasUsed": 10112,
    "from": "PQ7CzignhCH8YSsZLtvT6V56PY3tfXSKSd",
    "to": "PWTQCDnvXR37TzpppSW4FVJQywV3F1M9nF",
    "newContractAddress": null,
    "success": false,
    "returnValue": null,
    "bloom": "0000000000000000000000000000000000..",
    "error": "Stratis.SmartContracts.SmartContractAssertException: Only chairperson can give right 
to vote.\r\n at Stratis.SmartContracts.SmartContract.Assert(Boolean condition, String message)\r\n 
at Ballot.GiveRightToVote(Address voterAddress)",
    "logs": []
}

Now, we have three voters, and we have given the voting right to one voter, so, to provide the right to two other voters, repeat the process for voter2 and voter3.

Vote on Proposal

Then, a voter—with valid voting rights—can call the method “vote”. To call the method, we are using the same endpoint by changing parameters.

Parameters

Parameters Value Description
methodName Vote  
sender PQ7CzignhCH8YSsZLtvT6V56PY3tfXSKSd Voter1 wallet address
parameters [ "5#0" ] Voting proposal index.
“5#{proposal_index}” Here, the proposal is an array, so the first proposal index is 0, and for a second it is 1.

Repeat this for the second and third voter, but for the last voter, change the proposal index to 1. So, for the last voter, parameters should be something like this.

Parameters Value Description
methodName Vote  
sender PKuy4dacz3JYnEJiB43e1pNVHdHJscehEs Voter3 wallet address
parameters [ "5#1" ] Voting proposal index.
“5#{proposal_index}” Here, the proposal is an array, so the first proposal index is 0, and for a second it is 1.

So, 3 voters have been voted, two of them voted for proposal index 0, and one voter is voted for index 1. It’s time to get the winner’s name.

Get Winning Proposal

Let’s call the method “WinnerName”. Anyone can call this method, so we can pass any wallet address which has a balance. Let’s call it using the last voter’s wallet address.

Parameters

Parameters Value Description
methodName WinnerName  
sender PQ7CzignhCH8YSsZLtvT6V56PY3tfXSKSd Voter3 wallet address
parameters null The method “WinnerName” in SC doesn’t accept any parameter. So, passing null.

This will return the transaction id, that needs to pass to the smart contract receipt endpoint to get the result.

As we received two votes on index 0 and one vote on index 1, the first proposal is the winner.

You can find the source code here.

Summary

Stratis Platform is a blockchain product that offers a set of tools to build blockchain-based distributed apps (dApps). In this tutorial, we learned how to build a voting smart contract on our local development machine using Stratis Blockchain.

If you have doubts or get stuck at any point, please feel free to comment below.

Exercise for readers

  • Instead of the winning proposal method, add one more method like "End"; Using that method, the chairperson only can close the voting and get the result.
  • Add validation in the Vote method; if the proposal index is not present in available proposals.
  • Handle the scenario when all available proposals get the same number of votes.