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.
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:
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.
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.
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.
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.
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.
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.
A click of the button should increment the the displayed by one when the operation is included in the block.
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
Let's now build a contract a bit richer. Instead of merely incrementing, transform the contract to increment its storage by its given parameter.
The contract should now take a natural number as parameter.
[0n] * main(123n) -> [123n], []
[0n] * main(x) -> [x], [] if x is nat
[0n] * main(-1) -> type error
1n
in main
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 inputmethods.main(_parameter_).send()
to call a
contract with a given parameterLet'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.
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)
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
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.
When the Add
button is clicked, the value should
change according to the specification of the
contract. Same for the Sub
.
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.
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
contractset_add(_value_, _set_)
returns set in
which value was added.set_remove(_value_, _set_)
returns set
from which value was removed.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}], []
The interface should be composed of two buttons, Register and Unregister, as well as a list of registered users.
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.
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 msgsender: 'a' * [{registered: {}; max: 1n}] * register() -> [{registered: {'a'}; max: 1n}], []
sender: 'b' * [{registered: {'a'}; max: 1n}] * register() -> failure('No places left.')
The page should now display the number of places available.
The goal of this contract is to send the funds to the contract specified in the parameter that were transferred to the contract.
The contract should have a unique entrypoint, and not store anything.
Useful tidbits:
amount
returns the amount transferred in the calltransaction(_param_, _amount_, _contract_)
returns a transfer operationget_contract(_address_)
returns a contract
from an addressbalance
returns the amount of tokens stored on
the contractsender: 'a' * amount: 123 * main('b') -> ([], ['a' -> 'b' [123mutez | Unit]])
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:
return Tezos.contract.transfer({...params, amount, mutez:true})
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.
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.case _ of
like any sum type.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]]
The page should be comprised of three things:
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.
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.now: '2020-03-03' * [{timeout: '2020-01-01', ...}] * donate() -> failure("Time out.")
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.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.
The contract should now have three entrypoints:
The storage also needs to be extended. The contract must know about the goal as well as which project is being funded.
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.
Let's now do a page in which anybody can start their own funding.
Nothing needs to change in the contract. The application will also originate the contract instead of merely using it.
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