--- breaks: false --- # 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. ![dapp](https://gitlab.com/kinokasai/tezos-dapp-practicum/-/raw/master/dapp.png) ### 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](https://gitlab.com/kinokasai/tezos-dapp-practicum/-/tree/master/exos/counter/src/counter.ligo), is available on a [git repository](https://gitlab.com/kinokasai/tezos-dapp-practicum/). 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](https://ligolang.org/). Here is entirety of the smart contract code [counter.ligo](https://gitlab.com/kinokasai/tezos-dapp-practicum/-/tree/master/exos/counter/src/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](https://tezostaquito.io/), 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: ```const params = methods.donate(null).toTransferParams(); 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