Sign in
Log inSign up
Build A Decentralized Voting Application With Choice and Algorand Javascript SDK

Build A Decentralized Voting Application With Choice and Algorand Javascript SDK

farsh's photo
farsh
·Jan 14, 2022·

10 min read

What is Choice Coin?

Choice coin is an Algorand Standard Asset(ASA) built on the Algorand network to facilitate decentralized voting and governance.

In this tutorial, we are going to build a simple voting application using Javascript and Choice Coin.

Requirements

  • Nodejs installed on your computer
  • Knowledge of Javascript basics
  • Understanding of Blockchain basics
  • An Integrated Development Environment(IDE) e.g VSCode

1. Setup Project Directory and Install Dependencies

Create a new directory for the project. I’ll call mine choice-coin-voting-app. You can choose any name of your choice.

$ mkdir choice-coin-voting-app

initialize npm package in the directory

$ npm init -y

install the dependencies

$ npm install algo-sdk prompt-sync

Algosdk is the official JavaScript library for communicating with the Algorand network. Prompt-Sync module is a function that creates prompting functions, this prompts user to input variables from keyboard

2. Initialize Javascript-Algorand-Sdk

Create a main.js and import both modules

const algosdk = require('algosdk'); 
const prompt = require('prompt-sync')();

3. Configure Purestake API and initialize AlgodClient

Register for a Purestake developer account and get your API key to interact with algorand network.

const server = "testnet-algorand.api.purestake.io/ps2";
const port = "";
const token = {
  "X-API-Key": "YOUR API KEY", 
};
const algodClient = new algosdk.Algodv2(token, server, port)

4. Recover the escrow account with the Mnemonics

const mnemonic = "The mmemonic 25 characters seperated by a whitespace should be imported here";
const recoveredAccount = algosdk.mnemonicToSecretKey(mnemonic);
const escrow_address = prompt("Enter escrow address: ");
const CHOICE_ASSET_ID = 21364625;
const accountInfo = await algodClient
  .accountInformation(recoveredAccount.addr)
  .do();
const assets = accountInfo["assets"];

NOTE: The escrow_mnemonic provided must contain twenty-five(25) words separated by whitespace e.g “hello there and twenty more”

You might be thinking, what is escrow_address and escrow_mnemonic?

  1. Escrow Address: An Algorand wallet address is simply an account with sufficient $CHOICE to enable the voting process.
  2. Escrow Mnemonic: A 25-word pattern that is associated with an address upon creation. It is also known as a private key. It is useful for account recovery and therefore must be kept safe. To learn more about this, visit here. The pair of the escrow address & mnemonic is used to send choice coin to an option address(es).

To create a valid escrow account, the following steps are required:

  • Create a new wallet on WalletAlgo and select Testnet for this tutorial.
  • Copy the address of your newly created Algorand wallet and paste here to receive test Algos.
  • Swap the Algo received for Choice Coin on Tinyman
  • You’re set! Because a user should never be trusted, it is mandatory to validate the address and mnemonic provided.

5. Request User Input and Validate Escrow Address and Mnemonic

const validateEscrowWallet = (escrow_address) => {
  // compare the address to the address gotten from the mnemonic
  if (recoveredAccount.addr != escrow_address) {
    console.log("Invalid wallet address");
    return false;
  } else {
    //Check if choice coin is opted in
    if (assets.length > 0) {
      assets.map((asset) => {
        if (asset["asset-id"] != CHOICE_ASSET_ID) {
          console.log("Choice coin asset is not found, opt in choice coin ASA");
          return false;
        }
        //Check if it has sufficient funds
        else {
          const amount = asset["amount"];
          const choiceAmount = amount / 100;
          if (choiceAmount < 1000) return false;
        }
      });
    } else {
      console.log("No asset added yet, opt in choice coin ASA");
      return false;
    }
  }
  return true;
};

If the validation checks fail, the boolean false is returned. Otherwise, true is returned

6. Generate Voting Options/Decisions

Decisions are basically choices available in a voting process. In a typical election, candidates are the decisions available.

For the simplicity of this project, just two options are created. To create an option, the following requirements need to be met:

  • An Algorand account.
  • The account should be funded with a certain amount of Algo.
  • The account should be opted in to the Choice Coin ASA.
const option_one_address = ""; 
const option_zero_address = "";

7. Allow User Place Vote

With two options created in the previous step, user can now be prompted to make a choice between the available options.

const vote = async (option_zero_address, option_one_address) => {
  const params = await algodClient.getTransactionParams().do();
  const encoder = new TextEncoder();
  // Places a vote based on the input of the user.
  const votingOption = prompt("Vote 0 for zero and vote 1 for one: ");
  const amount = prompt("Please enter Amount to commit to voting:");

  if (votingOption == "1") {
    try {
      let txn = algosdk.makeAssetTransferTxnWithSuggestedParams(
        recoveredAccount.addr,
        option_one_address,
        undefined,
        undefined,
        amount * 100,
        encoder.encode("Voting powered by Choice Coin"),
        CHOICE_ASSET_ID,
        params
      );

      let signedTxn = txn.signTxn(recoveredAccount.sk);
      const response = await algodClient.sendRawTransaction(signedTxn).do();
      if (response) {
        console.log(
          `Thanks for voting for one,Your voting ID: ${response.txId}`
        );
        // wait for confirmation
        waitForConfirmation(algodClient, response.txId);
      } else {
        console.log("error voting for one, try again later");
      }
    } catch (error) {
      console.log("error voting for one, Try again later");
    }
  } else if (votingOption == "0") {
    try {
      let txn = algosdk.makeAssetTransferTxnWithSuggestedParams(
        recoveredAccount.addr,
        option_zero_address,
        undefined,
        undefined,
        amount * 100,
        encoder.encode("Voting powered by Choice Coin"),
        CHOICE_ASSET_ID,
        params
      );

      let signedTxn = txn.signTxn(recoveredAccount.sk);
      const response = await algodClient.sendRawTransaction(signedTxn).do();
      if (response) {
        console.log(
          `Thanks for voting for zero,Your voting ID: ${response.txId}`
        );
        // wait for confirmation
        waitForConfirmation(algodClient, response.txId);
      } else {
        console.log("error voting for  Zero, try again later");
      }
    } catch (error) {
      console.log("error voting for  Zero, Try again later");
    }
  } else {
    console.log("Please select a valid option");
  }
};

8. Wait For Confirmation

//verification function
const waitForConfirmation = async (algodClient, txId) => {
  let lastround = (await algodClient.status().do())["last-round"];
  while (true) {
    const pendingInfo = await algodClient
      .pendingTransactionInformation(txId)
      .do();
    if (
      pendingInfo["confirmed-round"] !== null &&
      pendingInfo["confirmed-round"] > 0
    ) {
      //Got the completed Transaction
      console.log(
        "Voting confirmed in round " + pendingInfo["confirmed-round"]
      );
      break;
    }
    lastround++;
    await algodClient.statusAfterBlock(lastround).do();
  }
};

9. Define the main function

Define a function called main. This serves as an entry-point for the voting process.

const main = async () => {
  // Entrypoint for the application
  const is_valid = validateEscrowWallet(address);
  if (!is_valid) console.log("Wallet does not meet the requirements.");
  else {
    vote(option_zero_address, option_one_address);
  }
};

10. Calculate Decisions Votes

After vote(s) have been placed, calculating the result is needed to provide numerical data. To calculate the result of a voting process after a user places a vote, we need to add the function calculateVotes to the code

const calculateVotes = async (addresses) => {
  // Calculate the result of a voting process.
  const results = [];
  for (var i = 0; i < addresses.length; i++) {
    const optionAccountInfo = await algodClient
      .accountInformation(addresses[i])
      .do();
    //get the account information
    const assets = optionAccountInfo["assets"];

    //Check if choice coin is opted in
    assets.map((asset) => {
      if (asset["asset-id"] != CHOICE_ASSET_ID) return false;
      else {
        const amount = asset["amount"];
        const choiceAmount = amount / 100;
        results.push(choiceAmount);
      }
    });
  }
  console.log(results);
  return results;
};

11. Determine Winner of Voting Process

A voting process without declaring a winner is absurd. As a result, a winner has to be determined after calculation of votes is done.

Due to the simplicity of the project, only a single vote is placed before the result is calculated, thereby making this process redundant.

However, in a larger application where several votes are placed by different users, the need for this process will be more obvious. To determine the winner, a function winner is defined and called after function main call:

const winner = (option_zero_count, option_one_count) => {
  // Selects a winner based on the result.
  if (option_zero_count > option_one_count) console.log("Option zero wins.");
  else if (option_zero_count < option_one_count)
    console.log("Option one wins.");
};

In a much larger application, a tie might occur amongst several users. Here, a winner is selected using a method from the Javascript standard library crypto. A more complex method can be used depending on the usecase.

12. Run the Application

Now that we have completed the logic of this application, it can be run in the terminal using

$ node main.js

The main.js looks like this:


const algodClient = new algosdk.Algodv2(token, baseServer, port);
// create a testnet account with myalgowallet, keep the mmemonic key;
const mnemonic = "";
const recoveredAccount = algosdk.mnemonicToSecretKey(mnemonic);
const address = prompt("Enter escrow address: ");
const CHOICE_ASSET_ID = 21364625;
const option_one_address = "";
const option_zero_address = "";

const accountInfo = await algodClient
  .accountInformation(recoveredAccount.addr)
  .do();
const assets = accountInfo["assets"];

const validateEscrowWallet = (address) => {
  // compare the address to the address gotten from the mnemonic
  if (recoveredAccount.addr != address) {
    console.log("Invalid wallet address");
    return false;
  } else {
    //Check if choice coin is opted in
    if (assets.length > 0) {
      assets.map((asset) => {
        if (asset["asset-id"] != CHOICE_ASSET_ID) {
          console.log("Choice coin asset is not found, opt in choice coin ASA");
          return false;
        }
        //Check if it has sufficient funds
        else {
          const amount = asset["amount"];
          const choiceAmount = amount / 100;
          if (choiceAmount < 1000) return false;
        }
      });
    } else {
      console.log("No asset added yet, opt in choice coin ASA");
      return false;
    }
  }
  return true;
};

const getBalance = async () => {
  //get the account information
  //Checks if the address is opted into Choice Coin and get choice amount from assets after voting
  assets.map((asset) => {
    if (asset["asset-id"] === CHOICE_ASSET_ID) {
      const amount = asset["amount"];
      const choiceAmount = amount / 100;
      console.log(
        `Account ${recoveredAccount.addr} has ${choiceAmount} $choice`
      );
      return choiceAmount;
    } else {
      console.log(
        `Account ${recoveredAccount.addr} must opt in to Choice Coin Asset ID ${CHOICE_ASSET_ID}`
      );
    }
  });
};

const vote = async (option_zero_address, option_one_address) => {
  const params = await algodClient.getTransactionParams().do();
  const encoder = new TextEncoder();
  // Places a vote based on the input of the user.
  const votingOption = prompt("Vote 0 for zero and vote 1 for one: ");
  const amount = prompt("Please enter Amount to commit to voting:");

  if (votingOption == "1") {
    try {
      let txn = algosdk.makeAssetTransferTxnWithSuggestedParams(
        recoveredAccount.addr,
        option_one_address,
        undefined,
        undefined,
        amount * 100,
        encoder.encode("Voting powered by Choice Coin"),
        CHOICE_ASSET_ID,
        params
      );

      let signedTxn = txn.signTxn(recoveredAccount.sk);
      const response = await algodClient.sendRawTransaction(signedTxn).do();
      if (response) {
        console.log(
          `Thanks for voting for one,Your voting ID: ${response.txId}`
        );
        // wait for confirmation
        waitForConfirmation(algodClient, response.txId);
      } else {
        console.log("error voting for one, try again later");
      }
    } catch (error) {
      console.log("error voting for one, Try again later");
    }
  } else if (votingOption == "0") {
    try {
      let txn = algosdk.makeAssetTransferTxnWithSuggestedParams(
        recoveredAccount.addr,
        option_zero_address,
        undefined,
        undefined,
        amount * 100,
        encoder.encode("Voting powered by Choice Coin"),
        CHOICE_ASSET_ID,
        params
      );

      let signedTxn = txn.signTxn(recoveredAccount.sk);
      const response = await algodClient.sendRawTransaction(signedTxn).do();
      if (response) {
        console.log(
          `Thanks for voting for zero,Your voting ID: ${response.txId}`
        );
        // wait for confirmation
        waitForConfirmation(algodClient, response.txId);
      } else {
        console.log("error voting for  Zero, try again later");
      }
    } catch (error) {
      console.log("error voting for  Zero, Try again later");
    }
  } else {
    console.log("Please select a valid option");
  }
};

//verification function
const waitForConfirmation = async (algodClient, txId) => {
  let lastround = (await algodClient.status().do())["last-round"];
  while (true) {
    const pendingInfo = await algodClient
      .pendingTransactionInformation(txId)
      .do();
    if (
      pendingInfo["confirmed-round"] !== null &&
      pendingInfo["confirmed-round"] > 0
    ) {
      //Got the completed Transaction
      console.log(
        "Voting confirmed in round " + pendingInfo["confirmed-round"]
      );
      break;
    }
    lastround++;
    await algodClient.statusAfterBlock(lastround).do();
  }
};

const calculateVotes = async (addresses) => {
  // Calculate the result of a voting process.
  const results = [];
  for (var i = 0; i < addresses.length; i++) {
    const optionAccountInfo = await algodClient
      .accountInformation(addresses[i])
      .do();
    //get the account information
    const assets = optionAccountInfo["assets"];

    //Check if choice coin is opted in
    assets.map((asset) => {
      if (asset["asset-id"] != CHOICE_ASSET_ID) return false;
      else {
        const amount = asset["amount"];
        const choiceAmount = amount / 100;
        results.push(choiceAmount);
      }
    });
  }
  console.log(results);
  return results;
};

const winner = (option_zero_count, option_one_count) => {
  // Selects a winner based on the result.
  if (option_zero_count > option_one_count) console.log("Option zero wins.");
  else if (option_zero_count < option_one_count)
    console.log("Option one wins.");
};

const main = async () => {
  // Entrypoint for the application
  const is_valid = validateEscrowWallet(address);
  if (!is_valid) console.log("Wallet does not meet the requirements.");
  else {
    vote(option_zero_address, option_one_address);
  }
};

main();
//A block takes approximately 3-4 seconds to be added to the blockchain. So, a delay is added to the to account for the synchronization. The delay function is described below:

await new Promise((r) => setTimeout(r, 30000));
getBalance();
const results = calculateVotes([option_one_address, option_zero_address]);
winner(results[0], results[1]);

Conclusion

Using the Choice Choin ASA and Algorand Javascript SDK, we were able to build a simple decentralized voting application.

The source code for this tutorial on Github. If you have any questions, feel free to reach out via Twitter.