Last edit 01/09/2022
Introduction
Smart contracts in the Tezos Blockchain are written in a low-level language called Michelson. Writing code in Michelson may quickly become tedious; it is like programming your daily tools with Assembly. However, multiple top-level languages generate Michelson, such as Ligo, Morely, and SmartPy, making smart contracts writing very productive and enjoyable.
This tutorial will use Ligo (but not directly) to generate Michelson code. Ligo accepts multiple syntaxes as input: JsLigo, ReasonLigo, PascaLigo, and CameLigo.
As the title suggests, we will write our smart contracts using OCaml with some added annotations and then generate a Ligo file using Mligo, a tool and a library that allow users to develop smart contracts under an OCaml environment (Tuareg, Merlin, Dune, Qcheck, etc.). After, we generate a Michelson file using Ligo and import this Michelson code inside Factori to interact with it.
Installation
To write smart contracts, we need to install Mligo, Ligo, and Factori.
Mligo
Mligo is not available in opam yet, but you can install it using:
$ opam pin add mligo.~dev git+https://gitlab.com/functori/dev/mligo
(say yes to the prompts and type eval $(opam env)
afterwards).
Mligo also contains a preprocessor to handle annotations and select
the correct code before generating the Ligo file. To add this
preprocess to your project, you need to add the command pps
mligo.ppx
to your preprocess stanza as shown below:
(library
...
(preprocess (pps mligo.ppx))
...
)
You will also need to start your smart contract with
open Mligo
Ligo
All installation instructions are explained on the official website of Ligo.
Factori
A tutorial is online which introduces Factori. It explains how to install Factori and interact with smart contracts using TypeScript. The installation part works for both OCaml and TypeScript output.
Example 1: Perfect numbers
The first smart contract that we will create is a simple number validator. First, a user (or a smart contract) will propose a number with a specific property (such as Prime Numbers, Perfect Numbers, Fibonacci number, etc.). If the provided number satisfies that property and exceeds the current number, then we reward the user with some XTZ. Otherwise, we fail the call of the smart contract.
The property chosen here is Perfect Number. A perfect number is a number which is equal to the sum of its divisors excluding itself. More information about this property can be found here.
To create our smart contract, we need to provide at least two pieces of information:
- The type of the storage
- The main entrypoint function.
Let's create our contract perfect_number.ml
in a new folder.
In order to benefit from the merlin
tool, let's create a dune
file
right away:
(library
(name perfect_number)
(modules perfect_number)
(preprocess (pps mligo.ppx))
)
We also need a dune-project
file or dune
will complain, all in the
same folder:
(lang dune 2.9)
For instance, for our first example, let's create a dune
file
Storage
As the logic behind our contract indicates, we need to check if the
provided number is greater than the current number. This leads to
having storage with a simple type int
:
type storage = int
Main entrypoint
The main entrypoint of a smart contract is a function that takes a specific type as an argument and returns the list of operations done by the smart contract and the state of the new storage. Here is its signature:
val main : params * storage -> operation list * storage
(not included in the actual smart contract).
where we define:
type params = Propose of int [@@entry Main]
Since our smart contract contains only one entrypoint, we have only
one constructor Propose
in the params
type.
The implementation of the main function will then have one case:
let main (action, store : params * storage) : operation list * storage =
match action with
Propose number -> (* handle entrypoint here. *)
Let's now implement the logic of the Propose
entrypoint:
Checking the property
This function will just verify the property: "n
equals the sum of
its divisors." It should be optimized since every operation running on
the Tezos Blockchain costs some XTZ:
let rec sum (acc : int) (n : int) (i : int) : int =
if i > n / 2 then
acc
else if n mod i = 0n then
sum (i + acc) n (i + 1)
else
sum acc n (i + 1)
let perfect_number (n : int) : bool =
sum 0 n 1 = n
Note that the type of n mod i
is nat
in Ligo, hence the 0n
which
will only understood by the OCaml compiler with the help of the
preprocessor. Another issue you can face with Ligo is getting
recursive functions to typecheck, since they need to be
tail-recursive.
The entrypoint itself
This function treats the entrypoint itself, and it should be called
from the main entrypoint (main
function):
let play (number, store : int * storage) =
if store < number then
if perfect_number number then
(* get the contract from the source address. *)
let source : (unit, storage) contract =
Tezos.get_contract_with_error None (Tezos.get_source None) "Source address not found." in
(* reward the user with 2000mutez. *)
let transaction : operation =
Tezos.transaction None unit 2000u source in
(* add the reward transaction the list of operations. *)
([transaction], number : operation list * storage)
else
(failwith "The number you provided it not a perfect number." : operation list * storage)
else
(failwith "The number you provided is smaller than the current perfect number." : operation list * storage)
Used functions
Tezos.get_source
: it returns the source address that called the contract. If atz1
called a smart contractkt1
and thiskt1
called anotherkt1'
, the source address insidekt1'
will be the firsttz1
.Tezos.get_contract_with_error
: it takes anaddress
and returns the associated contract. Another equivalent function could beTezos.get_contract_opt
which behaves likeTezos.get_contract_with_error
but returns(params, storage) contract option
.Tezos.transaction
: it makes a transfer (or a smart contract call) by providing the parameters of the entrypoint (if it is a contract), the amount (int tez
ormutez
) and the target address.
Note that every function used with Mligo (and all recursive functions)
should have a type annotation. Otherwise, Ligo will not be able to
typecheck the generated file. (This is true even of the failwith
instructions, which need to be given the type they are circumventing
by raising an error). Also, the first argument of the functions of the
Tezos
module is a context used to make tests, and it is None
in
our case (this is proper to Mligo
only, and this argument will be
deleted before the generation of the ligo
file).
Final smart contract file:
open Mligo
type storage = int
type params = Propose of int
[@@entry Main]
(* checking property. *)
let rec sum (acc : int) (n : int) (i : int) : int =
if i > n / 2 then
acc
else if n mod i = 0n then
sum (i + acc) n (i + 1)
else
sum acc n (i + 1)
let perfect_number (n : int) =
sum 0 n 1 = n
(* propose entrypoint. *)
let play (number, store : int * storage) =
if store < number then
if perfect_number number then
let source : (unit, storage) contract =
let source : address = Tezos.source None in
Tezos.get_contract_with_error None source "Source address not found." in
let transaction : operation =
Tezos.transaction None unit 2000u source in
([transaction], number : operation list * storage)
else
(failwith "The number you provided it not a perfect number." : operation list * storage)
else
(failwith "The number you provided is smaller than the current perfect number." : operation list * storage)
(* main entrypoint. *)
let main (action, store : params * storage) : operation list * storage =
match action with
| Propose number -> play (number, store)
You can already run dune build
to check that it compiles.
Generate Ligo file
Now that we have written our smart contract in OCaml, we can generate a Ligo file (with the extension .mligo in our case):
$ to_mligo perfect_number.ml
(generates a perfect_number.mligo
file).
We can make this automatic by adding some stanzas in our dune file:
(library
(name perfect_number)
(modules perfect_number)
(preprocess (pps mligo.ppx))
)
(rule
(deps perfect_number.ml)
(targets perfect_number.mligo)
(action (run to_mligo perfect_number.ml))
)
(you might need to delete perfect_number.mligo
before dune build
works again, because it won't want to overwrite your existing file).
Generate Michelson file
To get the Michelson file from the generated file perfect_number.mligo
:
$ ligo compile contract perfect_number.mligo > perfect_number.tz
If you used the rule in the above dune
file to generate perfect_number.mligo
, then it is in _build/default/
and you need to run
$ ligo compile contract _build/default/perfect_number.mligo > perfect_number.tz
Once again, let's make this more automatic by adding a rule to our dune file:
(rule
(deps perfect_number.mligo)
(targets perfect_number.tz)
(action (with-stdout-to perfect_number.tz (run ligo compile contract perfect_number.mligo -e main)))
)
(once again, you might need to delete perfect_number.tz
before dune build
works again, because it won't want to overwrite your existing file).
Click here to display the content of perfect_number.tz
{ parameter int ;
storage int ;
code { UNPAIR ;
DUP ;
DIG 2 ;
COMPARE ;
LT ;
IF { DUP ;
PUSH int 1 ;
DUP 3 ;
PUSH int 0 ;
PAIR 3 ;
LEFT int ;
LOOP_LEFT
{ UNPAIR 3 ;
PUSH int 2 ;
DUP 3 ;
EDIV ;
IF_NONE { PUSH string "DIV by 0" ; FAILWITH } {} ;
CAR ;
DUP 4 ;
COMPARE ;
GT ;
IF { SWAP ; DIG 2 ; DROP 2 ; RIGHT (pair int int int) }
{ PUSH nat 0 ;
DUP 4 ;
DUP 4 ;
EDIV ;
IF_NONE { PUSH string "MOD by 0" ; FAILWITH } {} ;
CDR ;
COMPARE ;
EQ ;
IF { PUSH int 1 ; DUP 4 ; ADD ; DUG 2 ; DIG 3 ; ADD }
{ PUSH int 1 ; DIG 3 ; ADD ; DUG 2 } ;
PAIR 3 ;
LEFT int } } ;
COMPARE ;
EQ ;
IF { PUSH string "Source address not found." ;
SOURCE ;
CONTRACT unit ;
IF_NONE { FAILWITH } { SWAP ; DROP } ;
PUSH mutez 2000 ;
UNIT ;
TRANSFER_TOKENS ;
SWAP ;
NIL operation ;
DIG 2 ;
CONS ;
PAIR }
{ DROP ;
PUSH string "The number you provided it not a perfect number." ;
FAILWITH } }
{ DROP ;
PUSH string "The number you provided is smaller than the current perfect number." ;
FAILWITH } } }
Factori
Now that we have a Michelson file. We will use Factori to import it and deploy it to the Tezos Blockchain.
To import a Michelson file in factori:
$ factori import michelson perfect_number perfect_number.tz --name pn --ocaml --force
If you used the rule in the above dune
file to generate perfect_number.mligo
, then it is in _build/default/
and you need to run
$ factori import michelson perfect_number _build/default/perfect_number.tz --name pn --ocaml --force
It will create a new project folder named perfect_number
:
perfect_number
└── src
├── libraries
│ ├── blockchain.ml
│ ├── dune
│ ├── factori_types.ml
│ └── utils.ml
├── ocaml_scenarios
│ ├── dune
│ └── scenario.ml
└── ocaml_sdk
├── dune
├── pn_code.ml
├── pn_code.mli
├── pn_ocaml_interface.ml
└── pn_ocaml_interface.mli
Successfully imported KT1.
It contains folders such as libraries
that include several OCaml
modules to interact with the blockchain, ocaml_sdk
contains the code
of the smart contract that you just imported and its OCaml interface
(type definitions, storage access, entrypoint calls, contract
deployment), and ocaml_scenarios
contains an empty scenario file you
need to fill if you want to have a scenario inside the factori
project.
Scenario
Below we write our scenario in
perfect_number/src/ocaml_scenarios/scenario.ml
.
Note that before our scenario can compile and run, we need to install the needed OCaml dependencies:
make -C perfect_number deps
make -C perfect_number ocaml
Whenever you want to run your scenario, you can simply run:
make -C perfect_number run_scenario_ocaml
Now let's deploy our contract inside the scenario. The function
deploy
is available in the Pn_ocaml_interface
module:
open Pn_ocaml_interface
open Tzfunc.Rp
let main () =
let>? perfect_number_kt1,_op_hash =
deploy
~node:Blockchain.ithaca_node
~name:"perfect_number"
~from:Blockchain.alice_flextesa
~amount:100000L
Z.one in
Format.printf "KT1: %s@." perfect_number_kt1;
Lwt.return_ok ()
let _ =
Lwt_main.run (main ())
This will deploy the contract to the ithacanet
node from the user
called alice_flextesa
(a tz1
address used initially by the
flextesa sandbox, but it is also available in the ithacanet).
Note that the value of the initial storage is a Z.t
since int
in
Michelson is a Z.t
even if we defined our storage as int
before we
generate the contract. Also, our smart contract rewards its users if
they provide a correct number; that is why we added 100000 mutez
to
its balance.
We have deployed our contract; let's interact with it by calling its entrypoint:
(* ... *)
let>? operation_hash =
call__default
~node:Blockchain.ithaca_node
~from:Blockchain.bob_flextesa
~kt1:perfect_number_kt1
(Z.of_int 28)
in
Format.printf "Operation Hash: %s@." operation_hash
(* ... *)
If the smart contract contains only one entrypoint, it will have the
name call__default
. This call will check if the number is greater
than the current number (the one inside the smart contract storage)
and then check if it is a perfect number. If both conditions are
satisfied, then it will return the hash of the operation, and you can
search for that hash by using an explorer such as
tzkt.io(make sure that you are in the
right network). If one of the conditions is not satisfied, then the
call to the smart contract will fail, and the node will return an
error according to your case:
- If the number is not greater than the current perfect number:
[Error in forge_manager_operations (call_entrypoint)]: {
"kind": "node_error",
"errors": [
{
"kind": "temporary",
"id": "proto.012-Psithaca.michelson_v1.runtime_error",
"contract_handle": "KT1FHSwJr8ijrZ8TpXKw76r19McZwhzkgoj8",
"contract_code": "Deprecated"
},
{
"kind": "temporary",
"id": "proto.012-Psithaca.michelson_v1.script_rejected",
"location": 146,
"with": {
"string": "The number you provided is smaller than the current perfect number."
}
}
]
}
- If the provided number is not a perfect number:
[Error in forge_manager_operations (call_entrypoint)]: {
"kind": "node_error",
"errors": [
{
"kind": "temporary",
"id": "proto.012-Psithaca.michelson_v1.runtime_error",
"contract_handle": "KT1FHSwJr8ijrZ8TpXKw76r19McZwhzkgoj8",
"contract_code": "Deprecated"
},
{
"kind": "temporary",
"id": "proto.012-Psithaca.michelson_v1.script_rejected",
"location": 140,
"with": {
"string": "The number you provided it not a perfect number."
}
}
]
}
While running a scenario with Factori; you may get some debug messages. You can disable them by:
let _ = Tzfunc.Node.set_silent true in
(* your scenario... *)
Final scenario file:
open Pn_ocaml_interface
open Tzfunc.Rp
let main () =
let _ = Tzfunc.Node.set_silent true in
Format.printf "Deploying the contract@.";
let>? perfect_number_kt1, _op_hash =
deploy
~node:Blockchain.ithaca_node
~name:"perfect_number"
~from:Blockchain.alice_flextesa
~amount:10000L
Z.one
in
Format.printf "KT1 : %s@." perfect_number_kt1;
let>? operation_hash =
call__default
~node:Blockchain.ithaca_node
~from:Blockchain.bob_flextesa
~kt1:perfect_number_kt1
(Z.of_int 28)
in
Format.printf "[Propose 28] Operation hash: %s@." operation_hash;
let>? operation_hash =
call__default
~node:Blockchain.ithaca_node
~from:Blockchain.bob_flextesa
~kt1:perfect_number_kt1
(Z.of_int 12)
in
Format.printf "[Propose 12] Operation hash: %s@." operation_hash;
Lwt.return_ok ()
let _ =
Lwt_main.run (main ())
You can find all the files of this example in Factori Examples inside the perfect_number folder. Note that a Makefile is provided.
Example 2: Split or Steal game
In this smart contract, we will implement a known game called Split or Steal (inspired by the prisoner's dilemma). It follows these rules:
- Two players play for a jackpot.
- Each player must secretly choose Split or Steal.
- If both choose Split, they each get a reward (half of the jackpot).
- If one chooses Split and the other chooses Steal, the one who chooses Steal will win all the jackpot, and the other will get nothing.
- If both choose Steal, none of the players get a reward.
To implement this smart contract, we will split it into four parts:
- Registration: Players will enter the game by calling an entrypoint and need to pay fees (the sum of the fees will be the jackpot (or almost)).
- Playing Both players will provide their answers secretly (i.e. without giving a plain text answer).
- Revealing Both players must reveal their answers to the smart contract if they played their turns.
- EndGame Both players have revealed their answers; it is time to reward them (or one of them) according to their answers.
As before, we recommend writing the dune
file right away (it will be
the same as in the previous example, replacing perfect_number
with
split_or_steal
).
Storage
The storage of this smart contract is a little bit more complicated than the previous one since we need to store multiple information about the players and their choices.
type state = Registration | Playing | Revealing | EndGame
type storage =
{
player1: address option; (* if a player has not entered the game yet this should be set to None. *)
player2: address option;
current_state: state; (* this could be : Registration, Playing, Revealing or EndGame. *)
choice1_hash: bytes; (* the answer should be hashed. *)
choice2_hash: bytes;
choice1_confirm: bool; (* to check if a player has already played. *)
choice2_confirm: bool;
choice1: string; (* the answer in raw format. *)
choice2: string;
current_players: int; (* number of current players (0, 1 or 2). *)
}
[@@comb]
Main entrypoint
In this smart contract, our main entrypoint does have multiple actions, and it will call the appropriate entrypoint for each case:
let main (action, s: params * storage) : operation list * storage =
match action with
| EnterGame -> enter_game ((), s)
| Play b -> play (b, s)
| Reveal (a, n) -> reveal((a, n), s)
| End -> endGame ((), s)
with
type params =
| EnterGame
| Play of bytes
| Reveal of string * string
| End
[@@entry Main]
Registration
To enter the game, the user must pay the fees (210 mutez
, for
example).
We will use the function Tezos.amount
to check if the
amount sent by the player is greater or equal 210 mutez
:
let enter_game (_, store : unit * storage) =
if store.current_state = Registration then
if Tezos.get_amount None < 210u then
(failwith "Registration fees has to be greater or equal than 210mutez (0.000210tez)" : operation list * storage)
else
begin
if store.current_players = 0 then
([] : operation list),
{
store with
player1 = Some (Tezos.get_source None);
current_players = 1
}
else if store.current_players = 1 then
([] : operation list),
{
store with
player2 = Some (Tezos.get_source None);
current_players = 0;
current_state = Playing
}
else
([] : operation list), store
end
else
(failwith "The game should be in REGISTRATION phase." : operation list * storage)
We assume that the game starts with Registration
phase. We change the state to Playing
if there are two registered players.
Playing
This entrypoint will store the hashed answer in the storage to prepare
the checking of the answer in the next phase (Revealing
) and prevent the same player from providing another answer:
let play (hashed_answer, store : bytes * storage) =
if store.current_state = Playing then
if store.player1 = Some (Tezos.get_source None) then
if store.choice1_confirm then
(failwith "You have already played." : operation list * storage)
else
([] : operation list),
{
store with
choice1_hash = hashed_answer;
choice1_confirm = true;
current_players = store.current_players + 1;
current_state = if store.current_players = 1 then Revealing else Playing
}
else if store.player2 = Some (Tezos.get_source None) then
if store.choice2_confirm then
(failwith "You have already played." : operation list * storage)
else
([] : operation list),
{
store with
choice2_hash = hashed_answer;
choice2_confirm = true;
current_players = store.current_players + 1;
current_state = if store.current_players = 1 then Revealing else Playing
}
else
(failwith "You are not registred as a player." : operation list * storage)
else
(failwith "The game is in PLAYING phase." : operation list * storage)
If both players played and provided their answers, we change the state to Revealing
.
Reveal
The answer that the players need to give to the smart contract needs
to be hashed so that they cannot cheat and see what their opponent has
played. After, to check the hash, the players need to send their
answers with a nonce (to prevent an attack by listing every hash of a
possible answer) to the smart contract. Finally, the smart contract
will check if the answers are correct by comparing the hash stored
from Playing
phase and the hash of the answer provided in the
Revealing
phase:
To create a hash, we could use the sha256
algorithm:
let get_hash ((secret, nonce), _ : (string * string) * storage) : bytes =
let secret_b, nonce_b = Bytes.pack secret, Bytes.pack nonce in
let phrase : bytes = Bytes.concat secret_b nonce_b in
Crypto.sha256 phrase
[@@view]
As you can notice, the annotation [@@view]
will let Ligo know that
this function is also a view, and it could be called outside the smart
contract (to check if the hash generated by the user is correct, for example).
We will use this view to check the provided hash inside the reveal
entrypoint:
let reveal ((answer, nonce), store : (string * string) * storage) =
if store.current_state = Revealing then
begin
if store.player1 = Some (Tezos.get_source None) then
if store.choice1_hash = get_hash((answer, nonce), store) then
([] : operation list),
{
store with
choice1 = answer;
current_state = if store.choice2 = "" then Revealing else EndGame
}
else
(failwith "Your (secret, nonce) do not match the stored hash." : operation list * storage)
else if store.player2 = Some (Tezos.get_source None) then
if store.choice2_hash = get_hash((answer, nonce), store) then
([] : operation list),
{
store with
choice2 = answer;
current_state = if store.choice1 = "" then Revealing else EndGame
}
else
(failwith "Your (secret, nonce) do not match the stored hash." : operation list * storage)
else
(failwith "You are not registred as a player" : operation list * storage)
end
else
(failwith "The game should be in REVEALING phase." : operation list * storage)
When we check the answer, we will also check if the other player has
already revealed his answer to change the state to EndGame
.
End
If both players registered, played and revealed their choices, someone
(even not a registered player) needs to call the endGame
entrypoint
to reward the appropriate player(s) (we could reward the players when
they reveal their choices but it is better to have another entrypoint
for that, in order to keep the code clean and easy to
understand). Also, to simplify the code, we will create a type for
answers:
type answer = Split | Steal | Unknown
with a function that returns the right answer from a string
:
let get_answer (s : string) =
if s = "Split" then Split
else if s = "Steal" then Steal
else Unknown
We will also need to reset the storage when the game ends:
let init_storage : storage =
{ current_state = Registration;
player1 : address option = None;
player2 : address option = None;
current_players = 0;
choice1_hash = Bytes.pack "";
choice2_hash = Bytes.pack "";
choice1_confirm = false;
choice2_confirm = false;
choice1 = "";
choice2 = ""; }
and finally, the endGame
entrypoint:
let endGame (_, store : unit * storage) =
if store.current_state = EndGame then
let p1 : (unit, storage) contract =
Tezos.get_contract_with_error None
(Option.unopt store.player1) "Problem with the address of Player 1"
in
let p2 : (unit, storage) contract =
Tezos.get_contract_with_error None
(Option.unopt store.player2) "Problem with the address of Player 2"
in
let ans1 = get_answer store.choice1 in
let ans2 = get_answer store.choice2 in
let store : storage = init_storage in
match ans1, ans2 with
| Split, Split ->
let op1 : operation = Tezos.transaction None unit 200u p1 in
let op2 : operation = Tezos.transaction None unit 200u p2 in
([op1; op2], store : operation list * storage)
| Split, Steal ->
let op2 : operation = Tezos.transaction None unit 400u p2 in
([op2], store : operation list * storage)
| Steal, Split ->
let op1 : operation = Tezos.transaction None unit 400u p1 in
([op1], store : operation list * storage)
| Steal, Steal ->
([], store : operation list * storage)
| _, _ ->
([], store : operation list * storage)
else
failwith "The game should be in END phase."
As you can see, we are not handling the Unknown
case in the
endGame
entrypoint to keep it simple. However, we can imagine a case
where the user does not provide the right answer(a string different
from "Split"
and "Steal"
). In this case, we will reward the one
who gave a correct answer which leads to:
(* ...the first four cases... *)
| Unknown, Split ->
let op2 : operation = Tezos.transaction None unit 100u p2 in
([op2], store : operation list * storage)
| Unknown, Steal ->
let op2 : operation = Tezos.transaction None unit 300u p2 in
([op2], store : operation list * storage)
| Split, Unknown ->
let op1 : operation = Tezos.transaction None unit 100u p1 in
([op1], store : operation list * storage)
| Steal, Unknown ->
let op1 : operation = Tezos.transaction None unit 300u p1 in
([op1], store : operation list * storage)
| Unknown, Unknown ->
([], store : operation list * storage)
Optimization
Our smart contract does not cover every possible case/scenario. For
example, one can provide an answer, say "Split"
, and when the game
reaches the revealing phase, the same player checks what the other one
played. If the other one chooses "Steal"
, this player could ruin the
game by not revealing his answer to the smart contract, which leads to
an infinite pause in the game. A solution could be using the level of
blocks (Tezos.get_level
) in the blockchain as a timeout or directly
Tezos.get_now
to get the current timestamp. However, we need to add
another entrypoint to trigger this check.
We could also change the storage of the smart contract to be more intuitive:
type player =
{
addr : address option;
hash : bytes;
confirm : bool;
answer : string;
}
type storage =
{
current_state : state;
current_players : int;
player1 : player;
player2 : player;
}
The smart contract could be extended to handle multiple players or to have various rounds for the same players. In this case, we just need to reset the storage for every round and change the reward amount regarding the number of rounds players have already played.
Scenario
Our smart contract is ready; let's use Factori to interact with it. If you haven't done so manually already, let's first generate the Michelson file by running
dune build
in our folder
(assuming that you produced a dune
file similar to the one in the
first example).
We will use alice_flextesa
and bob_flextesa
(two identities
available in the Blockchain
module and in both flextesa
and
ithacanet
networks) as players. Make sure that you create the
project using Factori:
$ factori import michelson split_or_steal split_or_steal.tz --name sos --ocaml --force
(once again, if you used the rule in the dune
file to generate split_or_steal.mligo
, then it is in _build/default/
and you need to run
$ factori import michelson split_or_steal _build/default/split_or_steal.tz --name sos --ocaml --force
Not that the scenario can indifferently be written inside the Factori
generated folders (src/ocaml_scenarios/scenario.ml
) or in a
split_or_steal_scenario.ml
file in our main folder, as is done in
the example files.
Let's create a set of variables that could be used several times in our scenario:
let node = Blockchain.ithaca_node in
let alice = Blockchain.alice_flextesa in
let bob = Blockchain.bob_flextesa in
(* ... *)
Deploy
To deploy the smart contract, we need to have an initial storage:
let init_storage =
{
player1 = None;
player2 = None;
current_players = Z.of_string "0";
choice1 = "";
choice2 = "";
choice1_confirm = false;
choice2_confirm = false;
choice1_hash = Crypto.H.mk "";
choice2_hash = Crypto.H.mk "";
current_state = Registration
}
And the deployment should be done the same way as the previous smart contract:
let>? kt1,_op_hash = deploy ~node ~name:"Split-or-Steal" ~from:alice init_storage in
Register
To register Alice
and Bob
we will use the entrypoint enterGame
of the module Sos_ocaml_interface
generated by Factori:
let>? alice_op = call_enterGame ~node ~amount:210L ~from:alice ~kt1 () in
Format.printf "Operation hash: %s@." alice_op;
let>? bob_op = call_enterGame ~node ~amount:210L ~from:bob ~kt1 () in
Format.printf "Operation hash: %s@." bob_op;
These two calls are independent, and while running scenarios in the
Tezos Blockchain, we need to wait for the confirmation of our
operations(wait for the construction of two blocks to get the
warranty). We could use the function Blockchain.parallel_calls
to
have both calls in parallel:
let reg_alice () = call_enterGame ~node ~amount:210L ~from:alice ~kt1 () in
let reg_bob () = call_enterGame ~node ~amount:210L ~from:bob ~kt1 () in
let>? _ =
Blockchain.parallel_calls
(Format.printf "Operation Hash : %s@.")
[reg_alice; reg_bob] in
This way, the two calls may end up in the same block, and we will only
wait for two blocks rather than four to have our confirmation. We will
use Blockchain.parallel_calls
in the rest of the scenario.
Creating a hash
As explained in the Play section of the smart contract, the players need to provide a hash; we can obtain the same hash locally using:
let compute_hash secret nonce =
let open Tzfunc.Rp in
let open Factori_types in
let$ secret_pack = Tzfunc.Forge.pack string_micheline (string_encode secret) in
let$ secret_nonce = Tzfunc.Forge.pack string_micheline (string_encode nonce) in
let phrase = Crypto.coerce (secret_pack) ^ (Crypto.coerce secret_nonce) in
Result.Ok (Digestif.SHA256.digest_bigstring @@ Bigstring.of_string @@ phrase)
let compute_string_hash secret nonce =
let open Tzfunc.Rp in
match (let$ hash = compute_hash secret nonce in
Result.Ok (Digestif.SHA256.to_hex @@ hash)) with
| Ok hash -> hash
| _ -> failwith "error in hash computation"
The function compute_string_hash
will return the same result as the
view get_hash
of the smart contract.
Let's create a hash for Alice
and Bob
:
let alice_choice, alice_secret = "Steal", "3N1C4Y" in
let alice_choice_hash = compute_string_hash alice_choice alice_secret in
let alice_choice_bytes = Crypto.H.mk alice_choice_hash in
let bob_choice, bob_secret = "Split", "04004873" in
let bob_choice_hash = compute_string_hash bob_choice bob_secret in
let bob_choice_bytes = Crypto.H.mk bob_choice_hash in
Play
Now that we have our hash answer for Alice
and Bob
, let's call the
play
entrypoint:
let play_alice () = call_play ~node ~from:alice ~kt1 alice_choice_bytes in
let play_bob () = call_play ~node ~from:bob ~kt1 bob_choice_bytes in
let>? _ =
Blockchain.parallel_calls
(Format.printf "Operation Hash : %s@.")
[play_alice; play_bob] in
Revealing the hash
Both players played. It is time to reveal their answers:
let rev_alice () = call_reveal ~node ~from:alice ~kt1 (alice_choice, alice_secret) in
let rev_bob () = call_reveal ~node ~from:bob ~kt1 (bob_choice, bob_secret) in
let>? _ =
Blockchain.parallel_calls
(Format.printf "Operation Hash : %s@.")
[rev_alice; rev_bob] in
Get rewards
Both players revealed their answers; now, everyone can call the
endGame
entrypoint. We will use Alice
since she is the one who won
the jackpot:
let>? end_hash = call__end ~node ~from:alice ~kt1 () in
Format.printf "Operation Hash : %s@." end_hash;
Notice that there are some entrypoints with two underscores after
call
, such as call__default
and call__end
. Both end
and
default
are OCaml keywords, and Factori needs to change them to
prevent type errors in OCaml. This should be fixed soon; for now,
users could check the interface of their smart contract and use the
proper name of the entrypoint.
Running the scenario
If we compile and run our scenario, a similar output could be:
--------------- Beginning of Split or Steal scenario ---------------
----------------------------- Contract -----------------------------
Deploying the contract...(KT1 : KT1ST6Haqh8pKVcpoLK82i4WvFXXkiJ3Ssse)
--------------------------- Registration ---------------------------
Alice & Bob entering the game...
Operation Hash : oo5cKAay6CEhzVgm7VWqH1HHcJcWyBGD8FpZ7FTAK1K83qNJFtK
Operation Hash : ooZs8RbMfz1S8AJ8MqGfjBytGuZuKQqqWzB3xHnmnEQxfrGr7E7
------------------------------ Playing -----------------------------
Alice & Bob are playing...
Alice playing with the hashed answer :
83685421ee208885a5cada044bec70932213a2eaf3163e3eb0caf8daa0834201
Bob playing with the hashed answer :
c18dcfb4752da291776b460cc4e631c8083170b9adba3c1a51d72ceb0fb89485
Operation Hash : oo4vLSurcLJhcESdqzJh3dWzLj4tDQkMKLxXEyDuYBHhKPVc5Ho
Operation Hash : opD5GD9wFc2nS596jPu4TTxwZgFChgKnfSq9hsqoZftWhs1zBBw
----------------------------- Revealing ----------------------------
Alice & Bob are revealing their choices...
Operation Hash : op9W1oVS4YgJhBgin7S4NgZbeSbLg2KYEATVrL5iKC3BtC38LnC
Operation Hash : ooCeJFg4JavWgeBvgoGyKKWHcjYBGWSLXHyavDFfPz5apyuc4Df
------------------------------ EndGame -----------------------------
Bob ending the game...
Operation Hash : op8Nm5gUFNQDXnvJKgsmdr4h2wN9rtErmpeU91SakPHPei3c78T
------------------ End of Split or Steal scenario ------------------
All the files of the Split or Steal game are available in Factori Examples inside the split_or_steal directory. Note that once again, a Makefile is provided.
Conclusion
We hope you have enjoyed trying out some of our finest tools for OCaml smart contract development: From writing your contract to running a scenario, it can all be done in one step with the help of a Makefile. We look forward to present our upcoming features such as automatic crawling capabilities, and a dynamic web page showing the state of each contract!