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

Block'n'Poll - Blockchain polling web application

Overview

Block’n’Poll is a blockchain polling web application that allows users to easily create permissionless polls and vote on Algorand blockchain.
This example uses mnemonics to sign transctions and that should not be done in production. In production integrate the webapp with AlgoSigner or the Pera Wallet API.

Why voting on blockchain?

Although voting processes require particular attention to privacy, especially in the government sphere, simple polls are instead a good use case for blockchain technology, which ensures a public, verifiable, tamper-proof and uncensored global voting process. Furthermore, a blockchain polling system enables use cases in which response incentive mechanisms can be built.

Even if the web app hosting domain is shut down, anyone still has the possibility to express his/her vote just using an Algorand node.

Algorand Java Web App

This solution aims to show how easy it is integrate Algorand into a full-stack Java web application through Algorand Java SDK. Polls are implemented as an Algorand Stateful Smart Contract: their TEAL logic validates both polls creation, opt-in and voting, avoiding double-voting for a same account. The Stateful ASC1 TEAL logic is derived from the Permissionless Voting Solution published on Algorand Developer Portal. Block’n’Poll expands the original TEAL source code in order to allow the creation of polls with more than two options.

Architecture

The web application is based on Java, integrating Spring Boot and Algorand Java SDK in order to interact with the blockchain. The application back-end has been developed following TDD approach and Hexagonal Architecture principles. A PostreSQL data-base is used just to speed up polls retrieving operations, while the blockchain is always the real source of truth. The front-end has been developed using ReactJS and communicates with the back-end through http calls towards the back-end REST API.

Back-End

Blockchain into Hexagonal Architecture

Accordingly with Hexagonal Architecture principles, the interactions with the blockchain takes place in dedicated repositories at the edge of the hexagon.

The application has three repositories:

  1. AlgorandASCPollRepository: is responsible for saving the polls’ Stateful ASC1. Below is reported the save() method that orchestrates other collaborators in order create poll’s application on the blockchain.

@Override
public BlockchainPoll save(Poll poll) {

  try {
    Account account = accountCreatorService.createAccountFrom(poll.getMnemonicKey());

    Transaction unsignedTx = unsignedASCTransactionService.createUnsignedTxFor(poll, account);

    String transactionId = transactionWriterService.write(account, unsignedTx);

    Long appId = algorandApplicationService.getApplicationId(transactionId);

    return pollBlockchainAdapter.fromPollToBlockchainPoll(poll, appId);

  } catch (IllegalArgumentException e) {
    logger
      .error("Something gone wrong creating account from mnemonic key creating poll {}.", poll,
             e);
    throw new InvalidMnemonicKeyException(e.getMessage());
  }
}

2 . AlgorandReadRepository: is responsible for retrieving information from the blockchain using the IndexerClient class. This repository has three methods that allow to the application to:
- check if an account is subscribed to a poll
- check if an account has already voted for a poll
- find poll informations (number of the accounts opted in, number of votes for each available options) from the blockchain

@Override
public Boolean isAccountSubscribedTo(OptinAppRequest optinAppRequest) {

  Response<AccountResponse> response;

  try {
    response = indexerClient.lookupAccountByID(optinAppRequest.getAccount().getAddress()).execute(headers, values);

  } catch (Exception e) {
    throw new AlgorandInteractionError(e.getMessage());
  }

  if (response.code() == 200) {
    return response.body().account.appsLocalState.stream()
      .anyMatch(app -> app.id == optinAppRequest.getAppId());
  } else {
    logger.error(
      "An error occurs calling algorand blockchain in order to retrieve account subscripted. Response code {}. Message {}. AppId {}. Address {}",
      response.code(), response.message(), optinAppRequest.getAppId(),
      optinAppRequest.getAccount().getAddress().toString());
    throw new AlgorandInteractionError(response.code(), response.message());
  }
}

@Override
public Boolean hasAddressAlreadyVotedFor(long appId, Address address) {
  Response<AccountResponse> response;
  try {

    response = indexerClient.lookupAccountByID(address)
      .execute(headers, values);
  } catch (Exception e) {
    throw new AlgorandInteractionError(e.getMessage());
  }

  if (response.code() == 200) {
    return response.body().account.appsLocalState.stream().filter(app -> app.id.equals(appId))
      .anyMatch(app -> app.keyValue.stream()
                .anyMatch(tealKeyValue -> tealKeyValue.key.equals(VOTED_REPRESENTATION)));
  }

  throw new AlgorandInteractionError(response.code(), response.message());
}

@Override
public ApplicationInfoFromBlockchain findApplicationInfoBy(BlockchainPoll poll) {
  Map<String, BigInteger> optionsVotes = findOptionsVotes(poll);

  // Introduced because of the limit imposed by purestake.
  // In an ipotetical production environment this problem should not exist
  try {
    Thread.sleep(API_TIME_DELAY);
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }
  List<Address> subscribedAddress = findAddressSubscribedToApplicationBy(poll.getAppId());
  return new ApplicationInfoFromBlockchain(optionsVotes, subscribedAddress.size());
}

3 . AlgorandWriteRepository: is responsible for writing to the blockchain voting and opt-in transactions:

@Override
public void optin(OptinAppRequest optinAppRequest){

  try{
    Transaction unsignedTransaction = buildOptinTransactionService.buildTransaction(optinAppRequest);

    transactionWriterService.write(optinAppRequest.getAccount(),unsignedTransaction);

  }catch(Exception e){
    logger.error("Something goes wrong sending transaction subscribing for app id {} from address {}",
                 optinAppRequest.getAppId(),optinAppRequest.getAccount().getAddress().toString(),e);
    throw e;
  }
}

@Override
public void vote(VoteAppRequest voteAppRequest){

  try{
    Transaction unsignedTransaction = buildVoteTransactionService.buildTransaction(voteAppRequest);

    transactionWriterService.write(voteAppRequest.getAccount(),unsignedTransaction);
  }catch(Exception e){
    logger.error("Something goes wrong sending vote transaction for app id {} from address {}",
                 voteAppRequest.getAppId(),voteAppRequest.getAccount().getAddress().toString(),e);
    throw e;
  }
}

TDD

The application has been developed following a TDD approach, that guided the evolution of the application itself.

The creation of several collaborators, each responsible for creating a specific type of transaction, is an implementation path that emerged thanks to TDD approach, which led to the separation of such collaborators from those collaborators responsible for signing and sending transactions to the blockchain. In this way the application relies on defined classes with single responsibility and more essential and specific tests.

Below is listed the method responsible for the opt-in transactions creation and their respective test.

public Transaction buildTransaction(OptinAppRequest optinAppRequest) {
        return Transaction.ApplicationOptInTransactionBuilder()
        .suggestedParams(blockchainParameterService.getParameters())
        .sender(optinAppRequest.getAccount().getAddress())
        .applicationId(optinAppRequest.getAppId())
        .build();
}
@Test
public void happyPath() {

  Transaction expectedOptinTransaction = aTransaction();

  context.checking(new Expectations(){{
    oneOf(blockchainParameterService).getParameters();
    will(returnValue(aTransactionParametersResponse()));
  }});

  Transaction transaction = buildOptinTransactionService
    .buildTransaction(new OptinAppRequest(APP_ID, account));

  assertThat(transaction, is(expectedOptinTransaction));
}

Key Examples

Noteworthy is the way in which the poll’s ASC1 is read, compiled and issued on the blockchain. The TEAL source code is read from the resources by the class TealTextGenerator. After reading the TEAL source code, the poll’s options defined during poll creation are appended to the TEAL source code. This allows Block’n’Poll to create polls with several voting options.

public static final String OPTIONS_PLACEHOLDER = "OPTIONS_PLACEHOLDER\n";
private final String APPROVAL_PROGRAM_PATH = "/teal/vote.teal";

private final String firstOptionTemplate = "txna ApplicationArgs 1\n"
  + "byte \"OPTION\"\n"
  + "==\n";

private final String optionTextTemplate = "txna ApplicationArgs 1\n"
  + "byte \"OPTION\"\n"
  + "==\n"
  + "||\n";

public String generateTealTextWithParams(List<String> options) {

  String firstOption = firstOptionTemplate.replace("OPTION", options.get(0));

  String otherOptions = options.stream().skip(1)
    .map(option -> optionTextTemplate.replace("OPTION", option))
    .collect(joining());

  return readFile(APPROVAL_PROGRAM_PATH)
    .replace(OPTIONS_PLACEHOLDER, firstOption.concat(otherOptions));
}

After the insertion of the options list into ASC1 source code, the Smart Contract is remotely compiled through a node who returns a TEALProgram, ready to be included in the ApplicationCall transaction.

public TEALProgram createApprovalProgramFrom(PollTealParams pollTealParams){

  return compileProgram(tealTextGenerator.generateTealTextWithParams(pollTealParams.getOptions()));
}

private TEALProgram compileProgram(String tealProgramAsStream){
  Response<CompileResponse> compileResponse;
  try{
    compileResponse=algodClient.TealCompile()
      .source(tealProgramAsStream.getBytes(UTF_8)).execute(headers,values);
  }catch(Exception e){
    throw new CompileTealProgramException(e);
  }

  return new TEALProgram(compileResponse.body().result);
}

The compiled TEALProgram, together with poll parameters and creator address, is passed to BuildApplicationCreateTransactionService, responsible for the creation of the unsigned ApplicationCreate transaction.

Below the buildTransaction() method is reported:

public Transaction buildTransaction(PollTealParams pollTealParams,
                                    TEALProgram approvalProgram, TEALProgram clearStateProgram, String sender){
  Transaction transaction;

  try{
    transaction=Transaction.ApplicationCreateTransactionBuilder()
      .sender(sender)
      .note(Bytes.concat(APPLICATION_TAG.getBytes(StandardCharsets.UTF_8),pollTealParams.getQuestion()))
      .args(arguments(pollTealParams))
      .suggestedParams(blockchainParameterService.getParameters())
      .approvalProgram(approvalProgram)
      .clearStateProgram(clearStateProgram)
      .globalStateSchema(new StateSchema(
        STATIC_GLOBAL_VARIABLES_NUMBER + pollTealParams.getOptions().size(),1))
      .localStateSchema(new StateSchema(0,1))
      .build();

  }catch(IllegalArgumentException e){
    logger.error("Something goes wrong with Sender Address transaction",e);
    throw new InvalidSenderAddressException(e.getMessage());

  }catch(Exception e){
    logger.error("Something goes wrong getting blockchain parameters transaction",e);
    throw new BlockChainParameterException(e.getMessage());
  }
  return transaction;
}

User Experience

Block’n’Poll’s web-app implements the following use-cases:

  1. Landing page

  2. Poll creation

  3. Poll visualisation

  4. Poll opt-in

  5. Poll voting

Landing page

In the landing page a dashboard shows created polls in small frames, highlighting the polls’ basic information. By clicking on any frame the user can open the poll’s page with more details.

EditorImages/2021/03/08 18:33/home.png

Poll creation

The user can create a poll, entering the required information like: poll question, poll name and poll options, specifying the time frame in which it is allowed to opt-in and vote for the poll. In this first version of the web-app, users interaction happens on the front-end through their mnemonic key (which is not stored in any way by the web app), signing required transactions to create poll’s Smart Contract application on the blockchain. On poll creations the question is saved on chain into transaction note field starting with the tag [blockandpoll][permissionless]. You can retrieve all the polls searching for the tag with Algorand Indexer.

EditorImages/2021/03/08 18:35/poll_creation.png

Poll visualization

The user can open a poll displayed in the landing page in order to get a detailed view of poll’s information, like: poll name, poll question, opt-in time frame, vote time frame, number of Algorand accounts subscribed to the poll and the number of votes received by each option. An histogram chart shows the votes summary.

EditorImages/2021/03/08 18:36/poll_visualization.png

Poll opt-in

Opt-In action is performed by the user entering the mnemonic key and clicking the opt-in button, if the opt-in phase has not expired yet and the user has not yet opted-in to the poll, the application returns a green confirmation message. Otherwise a red error message is shown.

Poll voting

Voting action is performed by the user selecting an option, entering the mnemonic key and clicking on the vote button in order to express the vote. If the user has not voted for the poll yet and the voting phase is still open, the application returns a green confirmation message. In case the user votes before the opt-in, Block’n’Poll will try to automatically perform opt-in on the users behalf: if opt-in phase is still open, Block’n’Poll performs opt-in and vote actions in sequence.

Managing your polls

Algodesk Application Manager is a very useful tool to manage your polls. From the Algodesk dashboard you can delete your own polls or close-out polls you no longer want to participate in. As per the polls’ TEAL logic, if a users closes-out their participation in a poll before the vote is over, their votes will be nullified.

Conclusions

Block’n’Poll aims to be an example of integration of Java web application based and Algorand Blockchain. The development of this web application has brought out some considerations on how to use the blockchain data, in particular about which information should be stored locally and which should always be read directly from the blockchain, bearing in mind that it always represents the single source of truth and should not be used as a large database.

The application has been built following this approach: to locally save those poll information that is never going to change (such as poll’s start and end date) and to systematically read the poll information that can change from the blockchain (such as the number of opt-in accounts or the number of votes for each option).

This approach ensures that any user with Internet connection can vote directly through an Algorand node (preserving the public and permissionless nature of the blockchain) while keeping Block’n’poll data consistency, displaying them as always read from the blockchain.

Other relevant considerations were made on how to dinamically handle ASC1 TEAL source code. The chosen approach simply starts from a TEAL poll template program (stored into artifact resources) and injects parameters specific for each poll (such as the voting options). For more complex ASC1, in production environments, where perhaps there are substantial changes of TEAL source code based on parameters used in the construction, one could think of delegating the construction process of the Smart Contract TEAL source code to specific classes or micro-service with this responsibility.