Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Solution Thumbnail

Build A Decentralized Voting Application With Choice and Algorand Python SDK

Overview

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 Python and Choice Coin.

Requirements

  • Python 3.x installed on your computer
  • Knowledge of Python 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 simple-choice-coin-app. You can choose any name of your choice.

$ mkdir simple-choice-coin-voting

Upon creation of your project directory, create a virtual environment for it using any preferred tool.

$ cd simple-choice-coin-voting && python -m venv .venv

Install the following dependencies inside your virtual environment.

$ pip install py-algorand-sdk

  • Py-Algorand-Sdk enables us interact with the Algorand network.

3. Initialize Py-Algorand-Sdk

Create a main.py and add the following code to the file.

from algosdk.v2client import algod


ALGOD_ADDRESS = "https://testnet.algoexplorerapi.io"
headers = {"User-Agent": "header"}
client = algod.AlgodClient("token", ALGOD_ADDRESS, headers)

Algorand TestNet API is being used here. Although extra headers & token aren’t required, they are needed to setup the sdk successfully.

4. Request User Input and Validate Escrow Address and Mnemonic

Define a function called main. This serves as an entrypoint for the voting process.

# main.py
def main():
    """Entrypoint for the application."""
    escrow_address = str(input("Enter escrow address: "))
    escrow_mnemonic = str(
        input("Enter escrow mnemonic (Each word should be separate by whitespace): ")
    )

    is_valid = validate_escrow_wallet(escrow_address, escrow_mnemonic, client)
    if not is_valid:
        print("Wallet does not meet the requirements.")

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.

# helpers.py
import time

from algosdk import account, mnemonic
from algosdk.encoding import is_valid_address
from algosdk.future.transaction import AssetTransferTxn, PaymentTxn

CHOICE_ASSET_ID = 21364625


def validate_escrow_wallet(address: str, _mnemonic: str, client) -> bool:
    """Validate an escrow wallet and check it has sufficient funds and is opted in to Choice Coin."""
    if not is_valid_address(address):
        return False

    # compare the address to the address gotten from the mnemonic
    if account.address_from_private_key(mnemonic.to_private_key(_mnemonic)) != address:
        return False

    if not contains_choice_coin(address, client):
        return False

    if get_balance(address, client) < 1000:
        return False

    return True


def get_balance(address: str, client):
    """Get the balance of a wallet."""
    account = client.account_info(address)
    return account["amount"]


def contains_choice_coin(address: str, client) -> bool:
    """Checks if the address is opted into Choice Coin."""
    account = client.account_info(address)
    contains_choice = False

    for asset in account["assets"]:
        if asset["asset-id"] == CHOICE_ASSET_ID:
            contains_choice = True
            break

    return contains_choice

If the validation checks fail, the boolean False is returned. Otherwise, True is returned.

5. 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.

The process of creating one to meet the above requirements is simplified using the following helper functions.

# helpers.py
def create_option_account(escrow_private_key: str, escrow_address: str, client) -> str:
    """Creates an account for option."""

    # This is the amount of Algo to fund the account with. The unit is microAlgos.
    AMOUNT = 1000000
    private_key, address = account.generate_account()

    is_successful = fund_address(AMOUNT, address, escrow_address, escrow_private_key, client)
    if not is_successful:
        raise Exception("Funding Failed!")

    is_optin_successful = opt_in_to_choice(private_key, address, client)
    if not is_optin_successful:
        raise Exception("Choice Coin Opt In Failed!")

    return address


def fund_address(
    amount: int, recipient_address: str, escrow_address: str, escrow_private_key: str, client
) -> bool:
    """Fund an account with Algo."""
    suggested_params = client.suggested_params()
    unsigned_transaction = PaymentTxn(
        escrow_address,
        suggested_params,
        recipient_address,
        amount,
        note="Initial Funding for Decision Creation",
    )
    signed_transaction = unsigned_transaction.sign(escrow_private_key)
    transaction_id = client.send_transaction(signed_transaction)

    return True


def opt_in_to_choice(private_key: str, address: str, client) -> bool:
    """Opt in a wallet to Choice Coin."""

    suggested_params = client.suggested_params()
    if not contains_choice_coin(address, client):
        unsigned_transaction = AssetTransferTxn(
            address, suggested_params, address, 0, CHOICE_ASSET_ID
        )
        signed_transaction = unsigned_transaction.sign(private_key)
        transaction_id = client.send_transaction(signed_transaction)

    return True

If any of the process(either funding the account or opting in) fails, the flow of the program is halted with an exception.

With the helper functions defined, let us add the following logic to create the required two options for our voting process inside main function.

# main.py

def main():
    """Entrypoint for the application."""
    # ...
    else:
        escrow_private_key = mnemonic.to_private_key(escrow_mnemonic)

        option_one_address = create_option_account(escrow_private_key, escrow_address, client)
        option_zero_address = create_option_account(escrow_private_key, escrow_address, client)

6. 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.

# main.py
def main():
    """Entrypoint for the application."""
    # ...
    vote(escrow_private_key, escrow_address, option_zero_address, option_one_address, client)

The vote function is defined in our helpers.py as:

# helpers.py
def vote(escrow_private_key, escrow_address, option_zero_address, option_one_address, client):
    """Places a vote based on the input of the user."""
    voter = int(input("Vote 0 for zero and vote 1 for one: "))
    if voter == 1:
        make_vote(
            escrow_address,
            escrow_private_key,
            option_one_address,
            100,
            "Voting Powered by Choice Coin.",
            client,
        )
        print("Thanks for voting for one.")
    else:
        make_vote(
            escrow_address,
            escrow_private_key,
            option_zero_address,
            100,
            "Voting Powered by Choice Coin.",
            client,
        )
        print("Thanks for voting for zero.")


def make_vote(sender, key, receiver, amount, comment, client):
    """Sends the transaction"""
    parameters = client.suggested_params()
    transaction = AssetTransferTxn(
        sender, parameters, receiver, amount, CHOICE_ASSET_ID, note=comment
    )

    signature = transaction.sign(key)
    client.send_transaction(signature)

    txn_id = transaction.get_txid()
    return txn_id

7. Calculate Decisions Votes

After vote(s) have been placed, calculating the result is needed to provide numerical data. To do this, we need to add the following to helpers.py:

# helpers.py

def calculate_votes(addresses: list, client):
    """Calculate the result of a voting process."""
    results = []
    for addr in addresses:
        account_info = client.account_info(addr)
        assets = account_info.get("assets")

        for _asset in assets:
            if _asset["asset-id"] == CHOICE_ASSET_ID:
                amount = _asset.get("amount")
                results.append(amount)

    return results

To calculate the result of a voting process after a user places a vote, the following is added to the main function:

# main.py 

def main():
    """Entrypoint for the application."""
    # ...
    option_one_count, option_zero_count = calculate_votes(
        [option_one_address, option_zero_address], client
    )

8. Wait For Blockchain to Sync

A block takes approximately four(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:

def wait_for_x_secs(delay: float) -> None:
    """Specify the number of seconds the program should delay for."""

    # A block takes approximately four(4) seconds to be added to the blockchain on Algorand
    print(f"Waiting for {delay} second(s) for blockchain to sync...")

    time.sleep(delay)

9. 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 is created in helpers.py:

# helpers.py

def winner(option_zero_count, option_one_count):
    """Selects a winner based on the result."""
    if option_zero_count > option_one_count:
        print("Option zero wins.")
    else:
        print("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 Python standard library secrets.choice(). A more complex method can be used depending on the usecase.

Implementing this logic in our main function is pretty straightforward:

# main.py

def main():
    """Entrypoint for the application."""
    # ...
    winner(option_zero_count, option_one_count)

10. Run the Application

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

$ python main.py

The main.py looks like this:

# main.py
from algosdk.v2client import algod

from helpers import (
    calculate_votes,
    create_option_account,
    mnemonic,
    validate_escrow_wallet,
    vote,
    wait_for_x_secs,
    winner,
)

ALGOD_ADDRESS = "https://testnet.algoexplorerapi.io"
ALGOD_TOKEN = ""
headers = {"User-Agent": "Blank!"}
client = algod.AlgodClient(ALGOD_TOKEN, ALGOD_ADDRESS, headers)


def main():
    """Entrypoint for the application."""
    escrow_address = str(input("Enter escrow address: "))
    escrow_mnemonic = str(
        input("Enter escrow mnemonic (Each word should be separate by whitespace): ")
    )

    is_valid = validate_escrow_wallet(escrow_address, escrow_mnemonic, client)
    if not is_valid:
        print("Wallet does not meet the requirements.")
    else:
        escrow_private_key = mnemonic.to_private_key(escrow_mnemonic)

        option_one_address = create_option_account(escrow_private_key, escrow_address, client)
        option_zero_address = create_option_account(escrow_private_key, escrow_address, client)

        vote(escrow_private_key, escrow_address, option_zero_address, option_one_address, client)

        wait_for_x_secs(5)

        option_one_count, option_zero_count = calculate_votes(
            [option_one_address, option_zero_address], client
        )

        winner(option_zero_count, option_one_count)


main()

The helpers.py also look like:

import time

from algosdk import account, mnemonic
from algosdk.encoding import is_valid_address
from algosdk.future.transaction import AssetTransferTxn, PaymentTxn

CHOICE_ASSET_ID = 21364625


def validate_escrow_wallet(address: str, _mnemonic: str, client) -> bool:
    """Validate an escrow wallet and check it has sufficient funds and is opted in to Choice Coin."""
    if not is_valid_address(address):
        return False

    # compare the address to the address gotten from the mnemonic
    if account.address_from_private_key(mnemonic.to_private_key(_mnemonic)) != address:
        return False

    if not contains_choice_coin(address, client):
        return False

    if get_balance(address, client) < 1000:
        return False

    return True


def get_balance(address: str, client):
    """Get the balance of a wallet."""
    account = client.account_info(address)
    return account["amount"]


def contains_choice_coin(address: str, client) -> bool:
    """Checks if the address is opted into Choice Coin."""
    account = client.account_info(address)
    contains_choice = False

    for asset in account["assets"]:
        if asset["asset-id"] == CHOICE_ASSET_ID:
            contains_choice = True
            break

    return contains_choice


def create_option_account(escrow_private_key: str, escrow_address: str, client) -> str:
    """Creates an account for option."""

    # This is the amount of Algo to fund the account with. The unit is microAlgos.
    AMOUNT = 1000000
    private_key, address = account.generate_account()

    is_successful = fund_address(AMOUNT, address, escrow_address, escrow_private_key, client)
    if not is_successful:
        raise Exception("Funding Failed!")

    is_optin_successful = opt_in_to_choice(private_key, address, client)
    if not is_optin_successful:
        raise Exception("Choice Coin Opt In Failed!")

    return address


def fund_address(
    amount: int, recipient_address: str, escrow_address: str, escrow_private_key: str, client
) -> bool:
    """Fund an account with Algo."""
    suggested_params = client.suggested_params()
    unsigned_transaction = PaymentTxn(
        escrow_address,
        suggested_params,
        recipient_address,
        amount,
        note="Initial Funding for Decision Creation",
    )
    signed_transaction = unsigned_transaction.sign(escrow_private_key)
    transaction_id = client.send_transaction(signed_transaction)

    return True


def opt_in_to_choice(private_key: str, address: str, client) -> bool:
    """Opt in a wallet to Choice Coin."""

    suggested_params = client.suggested_params()
    if not contains_choice_coin(address, client):
        unsigned_transaction = AssetTransferTxn(
            address, suggested_params, address, 0, CHOICE_ASSET_ID
        )
        signed_transaction = unsigned_transaction.sign(private_key)
        transaction_id = client.send_transaction(signed_transaction)

    return True


def vote(escrow_private_key, escrow_address, option_zero_address, option_one_address, client):
    """Places a vote based on the input of the user."""
    voter = int(input("Vote 0 for zero and vote 1 for one: "))
    if voter == 1:
        make_vote(
            escrow_address,
            escrow_private_key,
            option_one_address,
            100,
            "Voting Powered by Choice Coin.",
            client,
        )
        print("Thanks for voting for one.")
    else:
        make_vote(
            escrow_address,
            escrow_private_key,
            option_zero_address,
            100,
            "Voting Powered by Choice Coin.",
            client,
        )
        print("Thanks for voting for zero.")


def make_vote(sender, key, receiver, amount, comment, client):
    """Sends the transaction"""
    parameters = client.suggested_params()
    transaction = AssetTransferTxn(
        sender, parameters, receiver, amount, CHOICE_ASSET_ID, note=comment
    )

    signature = transaction.sign(key)
    client.send_transaction(signature)

    txn_id = transaction.get_txid()
    return txn_id


def calculate_votes(addresses: list, client):
    """Calculate the result of a voting process."""
    results = []
    for addr in addresses:
        account_info = client.account_info(addr)
        assets = account_info.get("assets")

        for _asset in assets:
            if _asset["asset-id"] == CHOICE_ASSET_ID:
                amount = _asset.get("amount")
                results.append(amount)

    return results


def winner(option_zero_count, option_one_count):
    """Selects a winner based on the result."""
    if option_zero_count > option_one_count:
        print("Option zero wins.")
    else:
        print("Option one wins.")


def wait_for_x_secs(delay: float) -> None:
    """Specify the number of seconds the program should delay for."""

    # A block takes approximately four(4) seconds to be added to the blockchain on Algorand
    print(f"Waiting for {delay} second(s) for blockchain to sync...")

    time.sleep(delay)

Conclusion

Using the Choice Choin ASA and Algorand Python 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.

Cover image source: Arnaud Jaegers