Basic concepts

Anatomy of a DApp

A DApp-short for Distributed Application-consists of several components. Primarily, an interface, usually web-based, which is designed for user interaction. The most important component of a dapp is the one (or many) smart contracts, which are stored on the blockchain. This part should ideally be minimal, as computation is replicated on every node. Most of the computation should be done in the client apps.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Smart contracts

Michelson smart contracts on the Tezos blockchain have three distinct components: the balance, the storage space, and the contract code itself. The balance denotes how many tokens the users have stored on the contract. The storage space is stored on the blockchain, enabling data to be persistent in between contract calls. Finally, the contract's code also distinguishes three components:

  • the parameters, which describe the available entrypoints to the contract,
  • the type of its storage,
  • and the actual Michelson instructions.

A Michelson contract has to respect a calling convention. Its input stack must be a tuple made of the parameter with which it is called and its storage. Its output must be a tuple made of the list of operations that need to be emitted after the contract is run, and its new storage value.

Each contract is moreover indexed by a specific id, which we also refer to as the contract's address.

Contract Specification

In the rest of this document, we will use the following syntax for specifying contracts:

[_storage_] * _entrypoint_(_value_) -> [_new-storage_], [_operations_]

On the left of ->, we describe the storage previous to the contract call, and the entrypoint with its arguments. On the right side, we describe the resulting storage, and the list of returned operations.

Exercises

The first application, counter.ligo, is available on a git repository. To check out this repository:

​​​​git clone https://gitlab.com/kinokasai/tezos-dapp-practicum/ 

Solutions are available on the solution branch. However, we recommend to really try to do the exercise and ask for help before looking at the solutions.

Exercise 1: Basic Math

Level 0 - Counter

The first contract that we are going to deploy is one of the simplest: a counter. Basically, the contract is going to track how many time it is called. Using the sintax defined above, the contract can be specified as follows:

[0] * main(Unit) -> [1], []

[13] * main(Unit) -> [14], []

[x] * main(Unit) -> [x + 1], []

where main names the contract's entrypoint. In other words, the counter contract increments the stored integer and returns an empty list of operations.

PascaLigo Implementation

In these series of excercises, we will implement the smart contracts using LIGO.

Here is entirety of the smart contract code counter.ligo:

type parameter is unit
type storage is nat

function main (const param: parameter; const s: storage)
  : (list(operation) * storage) is
  block {skip} with
  ((nil: list(operation)), s + 1n)
  • type parameter is unit: Here we define the parameter type of the contract. As the counter does not depend on any input, it is unit. In the functional world, unit describes a unique value. It is akin to void in C.

  • type storage is nat: This describes the shape of the storage. This is the value that will be preserved between calls, our counter. As it must necessarily be positive - a contract can't be called -1 times -, we use a natural number.

  • The signature of the entrypoint function main is given by:

    ​​​function main (const param: parameter; const s: storage) :
    ​​​              (list(operation) * storage) is
    

    We can recognize the Tezos calling conventions: parameter * storage tuple as input, and operations to be emitted * new storage for output.

  • block { skip } Each Pascaligo function consists of a number of instructions in a block. However, our counter needs no instructions, so it will only contain the do-nothing instruction, skip.

  • with ((nil: list(operation)), s + 1n) The right-hand side of the with clause describes the return value of the function. As our counter contract does not need to make further operations such as a transfer or an origination, we return an empty operation list, nil. Since nil could be of any type, we need to annotate it. Finally, we also return the new storage value, which is incremented by one (implementing the counter specification). Note that the literal is also annotated - 1n is a natural number, whereas 1 is an integer.

Web application

The interface of the counter dapp comprises of two elements, a number, representing the counter, and a button to call the contract.

import {Tezos} from '@taquito/taquito';
import {TezBridgeSigner} from '@taquito/tezbridge-signer';

These lines are needed to import Taquito, the library we use to communicate with a node.

// FIXME: Put your originated address here
let contract_address = "KT1MGDoCkk2L6zCfViRa8gzhFbxC3R877bUm";
var tk = Tezos;

First, some simple initialization. Later on, we will originate the counter contract. At that point, we need to replace contract_address with the address at which your contract was originated.

tk.setProvider({rpc: 'http://localhost:18731', signer: new TezBridgeSigner ()})

Here is the setup for Taquito. For ease development, we query a local sandbox node.

function render(elt) {
  document.querySelector('#info').innerHTML = elt
}

This function is used to render information on the web page, such as whereas the app is waiting for a signature, for the block to be included, etc

function update_storage() {
  tk.contract.at(contract_address)
  .then(contract => contract.storage())
  .then(storage => document.querySelector('#value').innerHTML = storage.toString())
}

This function queries the chain for the contract storage, and updates the page accordingly.

function call_contract() {
  tk.contract.at(contract_address)
  .then(contract => {
    render("Waiting for signature...")
    return contract.methods.main(null).send();})
  .then(op => {
    render("Sent! Waiting for confirmation...");
    return op.confirmation();})
  .then(block => {
    render("confirmed!")
    return update_storage(); })
  .catch(err => {
    console.log(err)
    render(err.message)
  })
  return null;
}

The real meat of the application. This function will get the script of the contract at a specific address, then will call a method on that contract. Note that the method called - main - is dependent on the name of our ligo function.

// Load the storage on page load
update_storage();
document.querySelector('#button').addEventListener('click', call_contract);

The final line links the button click event with a contract call.

Specification

A click of the button should increment the the displayed by one when the operation is included in the block.

Testing time!

First, we launch a tezos babylonnet sandbox. This can be beachieved using teztool.

If you don't have teztool, you can get it using docker:

​​​​docker pull registry.gitlab.com/nomadic-labs/teztool:latest

and then register it with the following alias:

​​​​alias teztool='docker run -it -v $PWD:/mnt/pwd -e MODE=dind \
​​​​    -e DIND_PWD=$PWD \
​​​​    -v /var/run/docker.sock:/var/run/docker.sock \
​​​​    registry.gitlab.com/nomadic-labs/teztool:latest'

Now we can launch the sandbox:

​​​​teztool babylonnet sandbox --baker bootstrap5 \
​​​​    --time-between-blocks 7 start 18731

We also need to install a Tezos client, which will allow us to communicate with the node. We use snap for this:

​​​​wget https://gitlab.com/abate/tezos-snapcraft/-/raw\/master/snaps/tezos_5.1.0_multi.snap?inline=false -o tezos_5.1.0_multi.snap
​​​​sudo snap install tezos_5.1.0_multi.snap --dangerous
​​​​export PATH=/snap/bin/:$PATH

Now we can talk with our node through the tezos.client command.

You may have to configure the client to use use the correct RPC port to communicate with the node launched by teztool:

​​​​tezos.client -A 127.0.0.1 -P 18731 config update

and then to register the bootstrap aliases:

tezos.client import secret key bootstrap1 unencrypted:edsk3gUfUPyBSfrS9CCgmCiQsTCHGkviBDusMxDJstFtojtc1zcpsh
tezos.client import secret key bootstrap2 unencrypted:edsk39qAm1fiMjgmPkw1EgQYkMzkJezLNewd7PLNHTkr6w9XA2zdfo
tezos.client import secret key bootstrap3 unencrypted:edsk4ArLQgBTLWG5FJmnGnT689VKoqhXwmDPBuGx3z4cvwU9MmrPZZ
tezos.client import secret key bootstrap4 unencrypted:edsk2uqQB9AY4FvioK2YMdfmyMrer5R8mGFyuaLLFfSRo8EoyNdht3
tezos.client import secret key bootstrap5 unencrypted:edsk4QLrcijEffxV31gGdN2HU7UpyJjA8drFoNcmnB28n89YjPNRFm

You can verify the connection between the client and the node by running:

​​​​tezos.client get balance for bootstrap1

If the client is acting funky, you may try to use the one if the teztool docker image.

​​​​alias tezos.client='teztool babylonnet sandbox client'

If you don't have LIGO, you can get it by running

# next (pre-release)
curl https://gitlab.com/ligolang/ligo/raw/dev/scripts/installer.sh \ 
    | bash -s "next"

Then, we originate the contract. This can be achieved by running make originate at the root of the folder.

In the receipt, you should see the address of the newly originated contract.

...
        Originated contracts:
          KT1M4guDEWUNXeJWKs45KVTodPkGfZno3E3T <------------------ HERE
        Storage size: 57 bytes
        Paid storage size diff: 57 bytes
        Consumed gas: 11480
        Balance updates:
          tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ0.057
          tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ0.257

New contract KT1M4guDEWUNXeJWKs45KVTodPkGfZno3E3T originated. <--- THERE
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
  tezos-client wait for ontcd6sAKcQd9GsgEegbWUVtdvAoxckZqWmgh21YdnaVFeuZWDS to be included --confirmations 30 --branch BKsixKod4BmB4C9zUJ3DBNJXRuwpT3jEVzbvKvKXNvTovqDiJF3
and/or an external block explorer.
Contract memorized as counter.

Using this address, you should now be able to replace the value of the contract_address variable in index.js.

The web application can be launched with yarn: yarn watch should launch the application at localhost:1234. Test that it works!

If you've never used Tezbridge, you may need to configure it to add a new account.

Here are the secret keys for each of the bootstrap accounts.

edsk3gUfUPyBSfrS9CCgmCiQsTCHGkviBDusMxDJstFtojtc1zcpsh edsk39qAm1fiMjgmPkw1EgQYkMzkJezLNewd7PLNHTkr6w9XA2zdfo edsk4ArLQgBTLWG5FJmnGnT689VKoqhXwmDPBuGx3z4cvwU9MmrPZZ edsk2uqQB9AY4FvioK2YMdfmyMrer5R8mGFyuaLLFfSRo8EoyNdht3 edsk4QLrcijEffxV31gGdN2HU7UpyJjA8drFoNcmnB28n89YjPNRFm

Level 1 - Sum

Let's now build a contract a bit richer. Instead of merely incrementing, transform the contract to increment its storage by its given parameter.

Ligo

The contract should now take a natural number as parameter.

Specification

[0n] * main(123n) -> [123n], []

[0n] * main(x) -> [x], [] if x is nat

[0n] * main(-1) -> type error

Hints

  • Change the type of the parameter
  • Do not just add 1n in main

Web

The page now gets a new element: an input. The value contained in this input should be transmitted to the contract when the button is clicked.

Useful tidbits:

  • document.querySelector('#input').value allows to get the value of an input
  • methods.main(_parameter_).send() to call a contract with a given parameter

Level 2 - Calc

Let's enrich our contract with the following feature. The user should now be able to either Increment or Decrement its storage according to its parameter value.

Ligo

In order to implement this behaviour, the contract should now offer two entrypoints: add and sub.

In Ligo, we can be express this using sum types. For instance, the following code snippet uses a sum type to denote how to set a boolean value.

type parameter is
  | SetTrue of unit
  | SetFalse of unit

type storage is bool

function main(const p: parameter, const s: storage) ... is
 block {
   case p of
   | SetTrue(x) -> s := true
   | SetFalse(x) -> s := false
 } with ((nil : list(operation)), s)

Specification

We extend the specification to consider the two different entrypoints:

[0] * add(123n) -> [123], []

[0] * sub(123n) -> [-123], []

[0] * add(x) -> [x], [] if x is nat

[0] * sub(x) -> [-x], [] if x is nat

Web

The change from a unique entrypoint to two is reflected in the presence of two buttons, labeled Add and Sub.

Useful tidbits:

  • In order to call a specific entrypoint in Taquito, one can use the following syntax:

    contract.methods._entrypoint_(_value_).send().

  • Remember that you can always check the names of the entrypoints in the Michelson .tz generated by the LIGO compiler.

Specification

When the Add button is clicked, the value should change according to the specification of the contract. Same for the Sub.

Registration

Level 0 - Infinite registration

Now for a different kind of contract. The goal here is to make a registration list for an event. The user should be able to register and unregister for an event.

Ligo

The contract should have two entrypoints: Register and Unregister. The contract should not handle the search for an registered user. If we are interested in implement this bahviour, it should be one at the web application layer.

Useful tidbits:

  • sender returns the address which called this contract
  • set_add(_value_, _set_) returns set in which value was added.
  • set_remove(_value_, _set_) returns set from which value was removed.

Specification

sender:'tz1x...q' * [{}] * register() -> [{'tz1x....q'}], []

sender:'tz1x...q' * [{'tz1x...q'}] * register() -> [{'tz1x....q'}], []

sender:'tz1f...f' * [{'tz1x...q'}] * register() -> [{'tz1x....q'; 'tz1f...f}], []

sender:'tz1x...q' * [{'tz1x...q'}] * unregister()-> [{}], []

sender:'tz1x...q' * [{'tz1f...f'}] * unregister() -> [{'tz1f...f}], []

Web

The interface should be composed of two buttons, Register and Unregister, as well as a list of registered users.

Level 1 - Bounded Registration

Right now, our registration application can handle an arbitrary large number of participants, that's not very realistic. Most events have a limited capacity. Thus, let's add a participants limit.

Ligo

The storage is now made of two components: the set of addresses registered, and the maximum limit of registration. This kind of structure can be represented using sum types. These are also called records or dictionaries.

In PascaLIGO, they are created using record. For example, the following record type stores Tezos networks, and the year they have been launched.

type storage is record
  network: string;
  launched_in: nat;
end

const mainnet_info: storage := record
  network: "mainnet";
  launched_in: 2018n;
end

Useful tidbits:

  • failwith(_msg_) makes the call fail with msg

Specification

sender: 'a' * [{registered: {}; max: 1n}] * register() -> [{registered: {'a'}; max: 1n}], []

sender: 'b' * [{registered: {'a'}; max: 1n}] * register() -> failure('No places left.')

Web

The page should now display the number of places available.

Tokens

Level 0 - Transfer proxy

The goal of this contract is to send the funds to the contract specified in the parameter that were transferred to the contract.

Ligo

The contract should have a unique entrypoint, and not store anything.

Useful tidbits:

  • amount returns the amount transferred in the call
  • transaction(_param_, _amount_, _contract_) returns a transfer operation
  • get_contract(_address_) returns a contract from an address
  • balance returns the amount of tokens stored on the contract
  • Operations to be executed are returned as output of the contract

Specification

sender: 'a' * amount: 123 * main('b') -> ([], ['a' -> 'b' [123mutez | Unit]])

Web

The interface should present the balance of two accounts, as well as a button to transfer some tokens from one account to the other.

Useful tidbits:

  • Due to a small bug in taquito, in order to transfer mutez, one should use the following code:
    ​​​​return Tezos.contract.transfer({...params, amount, mutez:true})
    

Level 1 - Donation

The aim of this contract is to do some fundraising. Anyone can donate tokens to a contract, from which one address can withdraw all the funds.

Ligo

The contract should have two entrypoints: Donate, and TakeAllTheMoney. Anybody can call Donate, as long as they transfer some amount of tokens. Donors should be stored associated with the total amount they donated. Only when the donatee, whose address is stored in the contract, calls that the contract transfers all the funds.

Useful tidbits:

  • map(address, nat) expresses a type linking an address to a natural number.
  • m[0] returns an option corresponding to Some(x) if the binding is in the map m, or None else.
  • Option types can be pattern matched with case _ of like any sum type.

Specification

sender: 'a' * amount: 0 * [{donators: {}, ...}] * donate() -> failure("No tokens transferred")

sender: 'a' * amount: 123 * [{donators: {}, ...x}] * donate() -> [{donators: { 'a': 123 }, ...x}], []

sender: 'a' * amount: 123 * [{donators: {'a' : 123}, ...x}] * donate() -> [{donators: { 'a': 246 }, ...x}], []

self.balance: 123 * sender: 'b' * [{donatee: 'b', ...x}] * takeAllTheMoney() -> [{donatee: 'b', ...x}], [self -> 'b' [123 | Unit]]

Web

The page should be comprised of three things:

  • Text indicating how much has been donated and how much is available
  • An input to specify how much one wants to donate in mutez
  • Two buttons: One for donating, the other for taking all the tokens

Level 2 - Time-bound donation

Some donations can only happen in a certain time period. Thus, we'll introduce a time restriction, after which donations should not able to happen.

Ligo

The storage should be extended with a timestamp representing the end of the donation period.

Useful tidbits:

  • timestamp is the name of the type representing timestamps.
  • now returns the current time according to the baker.

Specification

now: '2020-03-03' * [{timeout: '2020-01-01', ...}] * donate() -> failure("Time out.")

Web

The page should now display a message about the timeout. If timeout is attained, print "Donations ended on ${timeout}", else print "Donations end on ${timeout}"

Useful tidbits:

  • new Date() returns the date as of now.
  • new Date(timestamp) creates a new date object based on the timestamp.

Level 3 - Crowdfunding

Let's extend this contract to get a full-blown crowdfunding contract. A crowdfunding campaign works in the following way: Anyone can donate any non-zero number of tokens to support a project. At the end of the time allotted, two things may happen. If the objective is attained, the project manager gets all the tokens donated. If it is not, all donors can claim back what they gave.

Ligo

The contract should now have three entrypoints:

  • Pledge: Backers call that entrypoint to participate to the funding of the project
  • ClaimBack : Backers call that entrypoint if the funding period is over and the goal was not met to get back their money.
  • Fund: The project creator calls that entrypoint if the funding period is over and the goal was met

The storage also needs to be extended. The contract must know about the goal as well as which project is being funded.

Web

The main page is comprised of both persistent and modular information.

The persistent information is the same as the preceding level: information about the goal and how much tokens were given, the timeout message, and which project is currently being funded.

Actions available however depend on the application state. If the funding is ongoing then should only appear an input and the button Pledge. If the goal has been attained, only the Fund button is available. If the funding is over and the goal has not been met, only the button Claim Back is available.

Level 4 - Crowdfunding dashboard

Let's now do a page in which anybody can start their own funding.

Ligo

Nothing needs to change in the contract. The application will also originate the contract instead of merely using it.

Web

Our application needs to be extended with a new page. This page should take the shape of a form, in which the user can fill the desired information (hash of the project description, timeout, goal). When everything is filled and sent, the application should originate the contract and propose the user to go to their project page (what was developed on level 3