Complete dapp in 1 week!
Welcome to my 1-Week-Aepp-Challange.
The goal is to develop complete decentralized application (dapp) with Aeternity blockchain (aepp).
I’m documenting the whole design and development process.
Feel free to follow the progress.
Day 3: Logonity smart contract written in Sophia functional language (Aeternity smart contract language).
Day 2: https://www.mobycrypt.com/1-week-aeep-challange-day-2-aeternity-dapp-architecture-and-technologies/
Sophia
Sophia is strongly typed functional smart contract language developed by Aeternity. Based on ReasonML. Find out more here: https://github.com/aeternity/protocol/blob/master/contracts/sophia.md
Logonity smart contract
The Logonity smart contract has to contain given features:
- logo commission creation – anyone should be able to submit logo creation commission;
- logo commission reward storage – the principal of the commission, the user who creates the commission should be able to donate reward for the logo creation to the smart contract;
- won logo choosing and reward send – the smart contract must allow to choose won logo by the principal and automatically send the reward to the logo author; the functionality should cover edge cases like e.g. no logo proposals submitted;
- read the current commission state – there should be possibility to read current commission state (reward, the logo description, submitted proposal length);
The alpha version of the smart contract code which fulfills the requirements is given:
contract Logonity = record logoProposal = { author: address, logoUuidHash: string } record state = { commissionUuidHash: string, principal: address, logoDescription: string, submittedProposals: list(logoProposal), wonLogoUuid: string} public function init(commissionUuidHash: string, logoDescription: string) : state = { commissionUuidHash = commissionUuidHash, principal = Call.origin, logoDescription = logoDescription, submittedProposals = [], wonLogoUuid = ""} public stateful function submitProposal(logoUuidHash2: string) = put(state{submittedProposals = {author = Call.origin, logoUuidHash = logoUuidHash2}::state.submittedProposals}) public stateful function chooseWonLogo(wonLogoUuidHash:string) = if (Call.origin == state.principal) chooseLogoFromSubmittedOrSendRewardBackToPrincipal(wonLogoUuidHash) else abort("Only principal can choose the won logo!") private stateful function chooseLogoFromSubmittedOrSendRewardBackToPrincipal(wonLogoUuidHash: string) = if (length(state.submittedProposals) == 0) sendReward(state.principal) else switch(findTheLogo(state.submittedProposals, wonLogoUuidHash)) [] => sendRewardToFirstSubmittedProposal() head::tail => put(state{wonLogoUuid = wonLogoUuidHash}) sendReward(head.author) private stateful function sendRewardToFirstSubmittedProposal() = switch (state.submittedProposals) [] => () head::tail => put(state{wonLogoUuid = head.logoUuidHash}) sendReward(head.author) private function findTheLogo(submittedProposals: list(logoProposal), wonLogoUuidHash: string) :list(logoProposal) = switch(submittedProposals) [] => [] head::tail => if(head.logoUuidHash == wonLogoUuidHash) head::[] else findTheLogo(tail, wonLogoUuidHash) private function sendReward(recipient:address) = Chain.spend(recipient, Contract.balance) private function length(l : list('a)) : int = length'(l, 0) private function length'(l : list('a), x : int) : int = switch(l) [] => x head::tail => length'(tail, x + 1) public function getLogoDescription(): string = state.logoDescription public function getSubmittedProposalsLength(): int = length(state.submittedProposals) public function getReward(): int = Contract.balance
Let’s go through the all elements of the smart contract.
Line 1
contract Logonity =
is the definition of the contract. After this declaration in the new line the smart contract body is starting. There are no brackets. The new line code (starting from 2 line) must be at least 1 space after the code from line one. Generally every inner block code in Sophia language must be at least one space after the opening block code line.
Line 2 – 4
record logoProposal = { author: address, logoUuidHash: string }
Logo proposal structure definition. record
can be treated as the struct or class definition (object oriented programming) – it describes some data structure.
Line 6 – 11
record state = { commissionUuidHash: string, principal: address, logoDescription: string, submittedProposals: list(logoProposal), wonLogoUuid: string}
The record (structure) of the state – which is literally the inner application state. The state is the only part of the contract which contains persistent contract data. Changing the state requires the contract function call transaction. This state record definition is so far not yet the state definition (just it’s structure).
Line 13 – 18
public function init(commissionUuidHash: string, logoDescription: string) : state = { commissionUuidHash = commissionUuidHash, principal = Call.origin, logoDescription = logoDescription, submittedProposals = [], wonLogoUuid = ""}
Init method of the contract can be treated as the constructor – allows to initialize the contract with the data. It’s also initializing the state, setting the initial values (which later can be modified within contract functions).
The Logonity commission author creates the contract, and initializes it – providing the data (commissionUuidHash
generated by the backend server; principal
which is the address of the contract creator account, in our case the logo commission author; logoDescription
the description of desired logo, all the details about the logo; submittedProposals
will be the list of logo proposals submitted by artists, wonLogoUuid
will be set when the won logo will be selected).
Line 20 – 21
public stateful function submitProposal(logoUuidHash2: string) = put(state{submittedProposals = {author = Call.origin, logoUuidHash = logoUuidHash2}::state.submittedProposals})
The public function (public means we can invoke, interacting with the contract) which allows to submit logo proposal, by the logo author.
put(state{})
– this is how in Sophia we can modify the state. Within brackets should be the fields we modify. In our case we want to add element to the submittedProposals
list.
{author = Call.caller, logoUuidHash = logoUuidHash}::state.submittedProposals}
So far in the Sophia there is no API (like library) for list operations, we must do it manually – using Sophia language syntax & semantics. Lists in Sophia are immutable, it means that actually we cannot add elements to the list: we can glue together 2 immutable lists and finally get third immutable list (the result of list 1 + list 2). This code adds the new submittedProposal
record object to the state.submittedProposals
list. The operator ::
does the concatenation. The resulted list is assigned to the submittedProposals
field of our contract state.
Line 23 – 27
public stateful function chooseWonLogo(wonLogoUuidHash:string) = if (Call.origin == state.principal) chooseLogoFromSubmittedOrSendRewardBackToPrincipal(wonLogoUuidHash) else abort("Only principal can choose the won logo!")
The chooseWonLogo
function is responsible for choosing the won logo. The function takes the wonLogoUuidHash
as the argument. The hash comes from the Logonity backend server and is generated when user submit the logo to the Logonity – the logo file is saved in the backend server which generates the hash later used as the described wonLogoUuidHash
argument passed to the smart contract.
The first line of the function checks if the function caller is the logo principal – the author of the commission. Only the principal can invoke the function – only principal can choose won logo. If method call account is matching the principal account saved in the contract state then chooseLogoFromSubmittedOrSendRewardBackToPrincipal
function is invoked otherwise function finish with the error (abort
function).
Line 29 – 38
private stateful function chooseLogoFromSubmittedOrSendRewardBackToPrincipal(wonLogoUuidHash: string) = if (length(state.submittedProposals) == 0) sendReward(state.principal) else switch(findTheLogo(state.submittedProposals, wonLogoUuidHash)) [] => sendRewardToFirstSubmittedProposal() head::tail => put(state{wonLogoUuid = wonLogoUuidHash}) sendReward(head.author)
The function is responsible for choosing the winner and sending the reward. If there are no submitted any logo proposals, the reward is sent back to the commission principal (author). If there are logo proposals, the findTheLogo
function is invoked which should return single list with found element (if found by wonLogoUuidHash
) or empty list if no logo proposal with the hash found.
If there was not found logo proposal with the uuid submitted by the principal (wonLogoUuidHash
) the first logo proposal ever submitted to our contract will be chosen as the winner and will receive the reward. This is temporary solution, in future there should be random winner choice (Oracle will have to be added to the Logonity architecture).
If logo proposal with wonLogoUuidHash
was found – the winner receives the reward. State is updated with the won logo uuid hash.
Line 40 – 45
private stateful function sendRewardToFirstSubmittedProposal() = switch (state.submittedProposals) [] => () head::tail => put(state{wonLogoUuid = head.logoUuidHash}) sendReward(head.author)
The function chooses the single first element from the list as the commission winner (updating the state and sending the reward). The body of the function generally reflects how to access list elements in Sophia. We define switch with 2 possibilities: list is empty ([]
) and list contains elements (head::tail
). In our example when list is empty – we do nothing. When list has elements we take first element (head
) and process the rest of the logic, in our case updating state and sending the reward to the winner.
Line 47 – 54
private function findTheLogo(submittedProposals: list(logoProposal), wonLogoUuidHash: string) :list(logoProposal) = switch(submittedProposals) [] => [] head::tail => if(head.logoUuidHash == wonLogoUuidHash) head::[] else findTheLogo(tail, wonLogoUuidHash)
Function findTheLogo
is responsible for finding the element from submittedProposals
which has the logoUuidHash
property equal to wonLogoUuidHash
argument. Iterating through the array again is based on switch. The function returns list with single element.
head::tail => if(head.logoUuidHash == wonLogoUuidHash) head::[] else findTheLogo(tail, wonLogoUuidHash)
The serach algorithm is based on recursion. We check if head (first element in the list) is expected (if has expected logoUuidHash
). If yes we return the list (by concatenating empty list and head element head::[]
in the result having something like [head]
). If first element in the list is not expected, we use recursion – and invoke again the findTheLogo
function passing tail
as the argument. The tail
is the initial list without head
element. Such recursive algorithm should finally find expected element, or do nothing when finally the tail
argument will be just empty list.
Line 56 – 57
private function sendReward(recipient:address) = Chain.spend(recipient, Contract.balance)
Sophia has built-in functions to send transactions inside the smart contract. This line simply send the reward (current smart contract balance) to the given address.
Line 59 – 64
private function length(l : list('a)) : int = length'(l, 0) private function length'(l : list('a), x : int) : int = switch(l) [] => x head::tail => length'(tail, x + 1)
The code is responsible for returning the list length. Again based on the recursion. Feel free to read more about it Hack.bg blog: https://hack.bg/blog/tutorials/building-voting-aepp-with-sophia-ml-on-aeternity-blockchain/
Line 66 – 68
public function getLogoDescription(): string = state.logoDescription public function getSubmittedProposalsLength(): int = length(state.submittedProposals) public function getReward(): int = Contract.balance
Methods responsible for providing the state informations. All of them are read only functions, don’t change the smart contract state so invoking them is free (no paid fee).
Summary
Smart contract is the core of the dapp. Sophia based smart contracts are written in the functional paradigm.
The described Logonity smart contract covers required functionality however contains few flaws (functional and security), which I will describe in next articles. In the very next article I will describe the Aeternity SDK.
Great article! Keep up the good work 🙂
Thanks Milen for good feedback 😀