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://pthomann.pl/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 submittedProposalsfield 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 wonLogoUuidHashas 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 tailargument 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.


Also published on Medium.


Przemysław Thomann
Przemysław Thomann

Blockchain Freelancer, Consultant. Passionate. Blogger. Startups focused.

All author posts

Privacy Preference Center