From cebfaa67cc3178dbdbd4733e8a4f12e43ebb8c41 Mon Sep 17 00:00:00 2001 From: eltneg Date: Mon, 20 Jan 2020 13:39:34 +0100 Subject: [PATCH 1/2] Fix sophia-vote-contract --- sophia-vote-contract.md | 66 ++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/sophia-vote-contract.md b/sophia-vote-contract.md index fec9cd0..5eaa99d 100644 --- a/sophia-vote-contract.md +++ b/sophia-vote-contract.md @@ -6,11 +6,11 @@ This tutorial takes a look at a smart contract, written in Sophia ML for a votin ## 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 -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). @@ -26,15 +26,15 @@ contract Vote = record state = { votes: map(address, candidates) } - public stateful function init() : state = { -votes = { } } + stateful entrypoint 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. We start with the first functionality for the aepp – adding candidates: ````` - public stateful function add_candidate(candidate: address) : bool = +stateful entrypoint add_candidate(candidate: address) : bool = is_candidate(candidate) true `````` @@ -43,16 +43,16 @@ What this function does is passing the candidate to the `is_candiate()` function Here are the helper functions we are going to use for this: ````` - 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 = [] } }) +stateful function is_candidate(candidate: address) = + let candidate_found = lookup_by_address(candidate, state.votes, { voters = [] }) + if (size(candidate_found.voters) == 0) + put(state{ + votes[candidate] = { voters = [] } }) - private function lookup_by_address(k : address, m, v) = - switch(Map.lookup(k,m)) - None => v - Some(x) => x +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. @@ -61,19 +61,25 @@ We need to do this because in Sophia ML we don not have a default value of 0x0/f 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 +stateful entrypoint 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 `````` 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) +entrypoint 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) +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. @@ -82,10 +88,10 @@ Since in Sophia ML we don not have `.count` or `.length` to get the length of a 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) +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. @@ -100,37 +106,37 @@ contract Vote = record state = { votes: map(address, candidates) } - public stateful function init() : state = { + stateful entrypoint init() : state = { votes = { } } - public stateful function vote(candidate: address) : bool = + stateful entrypoint 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 = + entrypoint count_votes(candidate : address) : int = size(state.votes[candidate].voters) - private function size(l : list('a)) : int = size'(l, 0) + function size(l : list('a)) : int = size'(l, 0) - private function size'(l : list('a), x : int) : int = + function size'(l : list('a), x : int) : int = switch(l) [] => x _ :: l' => size'(l', x + 1) - public stateful function add_candidate(candidate: address) : bool = + stateful entrypoint add_candidate(candidate: address) : bool = is_candidate(candidate) true - private stateful function is_candidate(candidate: address) = - let candidate_found = lookupByAddress(candidate, state.votes, { voters = [] }) + stateful function is_candidate(candidate: address) = + let candidate_found = lookup_by_address(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) = + function lookup_by_address(k : address, m, v) = switch(Map.lookup(k,m)) None => v Some(x) => x From 9d8af936ec4e02867d0a821ac1adfd1504b3dc1e Mon Sep 17 00:00:00 2001 From: eltneg Date: Tue, 21 Jan 2020 14:53:51 +0100 Subject: [PATCH 2/2] Implement feedback: restructure vote-contract --- sophia-vote-contract.md | 161 ++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 95 deletions(-) diff --git a/sophia-vote-contract.md b/sophia-vote-contract.md index 5eaa99d..4ffa6c8 100644 --- a/sophia-vote-contract.md +++ b/sophia-vote-contract.md @@ -1,150 +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 **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 `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) } - stateful entrypoint init() : state = { + entrypoint 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. +``` -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. -````` -stateful entrypoint 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 }}) +``` -````` -stateful function is_candidate(candidate: address) = - let candidate_found = lookup_by_address(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. -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: -````` -stateful entrypoint 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. -````` -entrypoint 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: -````` -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. - -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. +```sophia +entrypoint count_votes(candidate : address) : int = + let candidate_found = Map.lookup_default(candidate, state.votes, { voters = [], exist = false }) + List.length(candidate_found.voters) +``` -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. -`````` -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) } - stateful entrypoint init() : state = { + entrypoint init() : state = { votes = { } } - stateful entrypoint 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 + 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 }) entrypoint count_votes(candidate : address) : int = - size(state.votes[candidate].voters) - - function size(l : list('a)) : int = size'(l, 0) - - function size'(l : list('a), x : int) : int = - switch(l) - [] => x - _ :: l' => size'(l', x + 1) - - stateful entrypoint add_candidate(candidate: address) : bool = - is_candidate(candidate) - true - - stateful function is_candidate(candidate: address) = - let candidate_found = lookup_by_address(candidate, state.votes, { voters = [] }) - if (size(candidate_found.voters) == 0) - put(state{ - votes[candidate] = { voters = [] } }) - - function lookup_by_address(k : address, m, v) = - switch(Map.lookup(k,m)) - None => v - Some(x) => x -````````` + let candidate_found = Map.lookup_default(candidate, state.votes, { voters = [], exist = false }) + List.length(candidate_found.voters) + -## 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. - + 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).