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:
AlgorandASCPollRepository
: is responsible for saving the polls’ Stateful ASC1. Below is reported thesave()
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:
-
Landing page
-
Poll creation
-
Poll visualisation
-
Poll opt-in
-
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.
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.
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.
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.