Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 71 additions & 94 deletions sophia-vote-contract.md
Original file line number Diff line number Diff line change
@@ -1,144 +1,121 @@
TUTORIAL: How to create a Sophia contract for a simple voting aepp?
====
# TUTORIAL: How to create a Sophia contract for a simple voting aepp

## Tutorial Overview

This tutorial takes a look at a smart contract, written in Sophia ML for a voting aepp but also provides another fundamental and deeper understanding about general basics of the language itself.

## Prequisites

- Installed **docker** (take a look at [this site](https://docs.docker.com/compose/install/) in case you didn't)
- Installed **aecli** (take a look at [this tutorial](https://github.com/aeternity/tutorials/blob/master/account-creation-in-ae-cli.md#installing-aecli) to remind yourself on installing the javascript version of aecli)
- Installed **forgae** (take a look at [this section](https://github.com/aeternity/tutorials/blob/master/smart-contract-deployment-in-forgae.md#installing-forgae))
- Installed **aeproject** (take a look at [this section](https://github.com/aeternity/aepp-aeproject-js))

## Setting up project and development environment

## Setting up project and development environment
First we have to initialize our project where we write our smart contract. In order to do this we are going to use `forgae`.
First we have to initialize our project where we write our smart contract. In order to do this we are going to use `aeproject init`.

## Smart contract
In Sophia ML we have a state which is the place to store data on-chain - and it is the only part in the smart contract that can be mutated (overwritten).

The first thing we are going to do is to define our variables and types that we use in the smart contract. Besides that we are going to define the `init()`function, which is the constructor basically, if we would compare this to a Solidity smart contract.
In Sophia we have a state which is the place to store data on-chain - it is the only part in the smart contract that can be mutated (overwritten).

````
The first thing we are going to do is to define the state variables and types that we will use in the smart contract. Besides that we are going to define the `init()`function, which is a constructor.

```sophia
contract Vote =

record candidates = {
voters: list(address) }
voters: list(address),
exist: bool}

record state = {
votes: map(address, candidates) }

public stateful function init() : state = {
votes = { } }
`````
The `candidate` record stores it's **voters** in a **list** of addresses. The `state` record stores all the **votes** in a mapping (which is basically a key-value pair) of address to candidate record.
entrypoint init() : state = {
votes = { } }
```

We start with the first functionality for the aepp – adding candidates:
The `candidate` record stores it's **voters** in a **list** of addresses. The `state` record stores all the **votes** in a mapping (which is basically a key-value pair) of address to candidate record. The candidate record has a field, exist, that will allow us to check the existence of a candidate in the state record.

`````
public stateful function add_candidate(candidate: address) : bool =
is_candidate(candidate)
true
``````
What this function does is passing the candidate to the `is_candiate()` function – taking a candidate’s address as a parameter. Then the function checks if there is a candidate defined with this address and saves it to the **votes** mapping in the state with the initial empty list of voters (in case if is not).
We start with the first functionality for the aepp – adding candidates:

Here are the helper functions we are going to use for this:
```sophia
stateful entrypoint add_candidate(candidate: address) =
if (!(is_candidate'(candidate)))
put(state{votes[candidate] = { voters = [], exist = true }})
```

`````
private stateful function is_candidate(candidate: address) =
let candidate_found = lookupByAddress(candidate, state.votes, { voters = [] })
if (size(candidate_found.voters) == 0)
put(state{
votes[candidate] = { voters = [] } })
What this function does is that it passes the new candidate's address to the `is_candiate` function. The `is_candidate` function then checks if there is a candidate defined with this address. If there isn't, `add_candidate` will save the new candidate's address it to the **votes** mapping in the state with an initial empty list of voters and exist field set to true.

private function lookup_by_address(k : address, m, v) =
switch(Map.lookup(k,m))
None => v
Some(x) => x
``````
We need to do this because in Sophia ML we don not have a default value of 0x0/false as in Solidity for example. So to be able to cast a vote, we need to have add the candidates first whom we can vote for.
Here are the helper functions we are going to use for this:

```sophia
entrypoint is_candidate(candidate: address) : bool =
is_candidate'(candidate)

**If we do not add the candidate first before voting, we will hit out of gas error.**
function is_candidate'(candidate: address) : bool =
let candidate_found = Map.lookup_default(candidate, state.votes, { voters = [], exist = false })
candidate_found.exist
```

Next we create the vote function which looks basically like this:
`````
public stateful function vote(candidate: address) : bool = is_candidate(candidate) let new_votes_state = Call.caller :: state.votes[candidate].voters put(state{ votes[candidate].voters = new_votes_state }) true
``````

```sophia
stateful entrypoint vote(candidate: address) =
if (is_candidate(candidate))
let current_votes = state.votes[candidate].voters
put(state{ votes[candidate].voters = Call.caller :: current_votes })
```

We access the transaction initiator’s address by the built in `Call.caller` and prepend it `::` to the current list of voters.

The last step is to create a `get votes count` function.
`````
public function count_votes(candidate: address) : int = size(state.votes[candidate].voters)
`````

We need to make use of a `size` function which we define as a helper function below. Here is the code:
`````
private function size(l : list('a)) : int = size'(l, 0)
`````

This is where things get a bit more complicated, so I will try to explain what is happening here actually.
```sophia
entrypoint count_votes(candidate : address) : int =
let candidate_found = Map.lookup_default(candidate, state.votes, { voters = [], exist = false })
List.length(candidate_found.voters)
```

Since in Sophia ML we don not have `.count` or `.length` to get the length of a list, we need to make ourselves a helper function which makes a recursion and iterates over the list while incrementing a counter.

The `size` function is defined to accept a list of `'a` which is the convention for a generic type, and a return type `int` . In the function’s body we are calling the `size'` function while passing the list and an initial value for the counter.
``````
private function size'(l : list('a), x : int) : int =
switch(l)
[] => x
_ :: l' => size'(l', x + 1)
``````
And here the magic happens: We use the `switch` statement with 2 cases `[] => x` – which returns the value of the counter and breaks the recursion in case the list is empty. The last part `_ :: l' => size'(l', x+1)` matches pattern and separates the first element from the list and the remainder. Then it passes recursively the list’s remainder to the same function, while incrementing the counter.
`Map.lookup_default` will either return the cadidate's record stored in the votes map of the state record if the candidate exist or a candidates's record with an empty list of voters and an exist field with a false value. We then use `List.length` to get the number of voters in the voters's list.

The final smart contract code looks like this in the end:

```````
```sophia
contract Vote =

record candidates = {
voters: list(address) }
voters: list(address),
exist: bool}

record state = {
votes: map(address, candidates) }

public stateful function init() : state = {
entrypoint init() : state = {
votes = { } }

public stateful function vote(candidate: address) : bool =
is_candidate(candidate)
let new_votes_state = Call.caller :: state.votes[candidate].voters
put(state{
votes[candidate].voters = new_votes_state })
true

public function count_votes(candidate : address) : int =
size(state.votes[candidate].voters)

private function size(l : list('a)) : int = size'(l, 0)

private function size'(l : list('a), x : int) : int =
switch(l)
[] => x
_ :: l' => size'(l', x + 1)

public stateful function add_candidate(candidate: address) : bool =
is_candidate(candidate)
true

private stateful function is_candidate(candidate: address) =
let candidate_found = lookupByAddress(candidate, state.votes, { voters = [] })
if (size(candidate_found.voters) == 0)
put(state{
votes[candidate] = { voters = [] } })

private function look_up_by_address(k : address, m, v) =
switch(Map.lookup(k,m))
None => v
Some(x) => x
`````````
stateful entrypoint vote(candidate: address) =
if (is_candidate(candidate))
let current_votes = state.votes[candidate].voters
put(state{ votes[candidate].voters = Call.caller :: current_votes })

## Conclusion
It is fairly simple to create a basic aepp on æternity blockchain using Sophia ML. In our case with the ae-vote we stumbled upon some tricky parts like the recursive iteration which we had to make above. But it gets easier with time if you familiarize yourself with the language. In case you encounter any problems feel free to contact us through the [æternity Forum](https://forum.aeternity.com/c/development) please.

entrypoint count_votes(candidate : address) : int =
let candidate_found = Map.lookup_default(candidate, state.votes, { voters = [], exist = false })
List.length(candidate_found.voters)


stateful entrypoint add_candidate(candidate: address) =
if (!(is_candidate'(candidate)))
put(state{votes[candidate] = { voters = [], exist = true }})

entrypoint is_candidate(candidate: address) : bool =
is_candidate'(candidate)

function is_candidate'(candidate: address) : bool =
let candidate_found = Map.lookup_default(candidate, state.votes, { voters = [], exist = false })
candidate_found.exist

```

## Conclusion

It is fairly simple to create a basic aepp on æternity blockchain using Sophia ML. It even gets easier with time if you familiarize yourself with the language. In case you encounter any problems feel free to contact us through the [æternity Forum](https://forum.aeternity.com/c/development).