Liquidity Tutorials — Hands-on Series — Part 1

Photo by Darwis Alwan from Pexels

This is the first part of a tutorial series which provides a hands-on, step-by-step guide to writing smart contracts in Liquidity. In each part, we will write a Liquidity contract inspired by an example in the Michelson “A contract a day” series. Michelson has evolved since the Michelson series was written, so the contracts in that series may not necessarily run as written.

Liquidity is a high-level typed smart-contract language for Tezos that strictly complies with Michelson security restrictions. Anything that can be written in Michelson can be written in Liquidity. OCaml developers will find the syntax of Liquidity familiar, because it has OCaml-like syntax. However, this tutorial is self-contained, and does not require any Liquidity or OCaml knowledge.

We’ll first review what a Liquidity contract typically contains, then we’ll start writing our first contract! After that we’ll compile and run an instance of the contract.

Components of a Liquidity contract

A Liquidity contract has the following components:

  • storage: stored state of the contract.
  • parameter(s): values that must be provided when you call the contract.
  • entry point(s): one or more entry points can be defined. The entry point with the name main is the default.
  • return values: the entry points always return a pair of a list of operations and storage.
  1. operation is a type in Liquidity. We’ll cover this in more details later on. For now, note that the entry points return a list of operations, i.e., it is of type operation list.
  2. the return storage represents the final state of the contract, it is of type storage.

Liquidity contracts typically have the following structure:

  1. type declarations, the storage’s type must be declared. Liquidity can infer the parameters’ types. Use the syntax type storage = (type name).
  2. local values definitions using the syntax let (variable name) = ....
  3. initializer, you can initialize the storage with the syntax let%init storage (inputs, if any) =.
  4. entry points, define entry points with the syntax:
let%entry 
(name of the entry point)
(parameter: annotation)
(storage) =
body, returning ([list of operations], storage)

And that’s it! Let’s see an example!

The identity contract

We start with a very simple example here, a contract that doesn’t have any effect:

(* Identity contract for strings. *)

type storage = unit

let%entry main
(_parameter : string)
storage =
( [], storage )

(* Initialize storage to (). *)
let%init storage = ()

Save this contract with a .liq file extension. In this contract, the storage type is unit. There is only one entry point: main. It takes in a parameter of type string. The return value is a pair of an empty (operation) list and the storage, which is of type unit. Type unit can only have one value in Liquidity, and it is unit, or ().

Compile and run the contract

To compile the .liq file, we need Liquidity installed. Install Liquidity as per the installation instructions. Now, we can compile the file, which I saved as id_strings.liq, to id_strings.tz by running:

liquidity [path to]/id_strings.liq

The output should look like this:

Main contract Id_strings
Constant initial storage generated in "./id_strings.init.tz"
File "./id_strings.tz" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ./id_strings.tz

If you view the .tz file, you can see the compiled Michelson code:

parameter string;
storage unit;
code { CDR @storage_slash_1 ; NIL operation ; PAIR };

Run a simulation with Liquidity

Technically you don’t even need to compile the .liq file to a Michelson file (.tz) because Liquidity has a built-in Tezos client. You need to have a Tezos node running (consult this if you require assistance on this). In the below example we have a local node running:

$ liquidity \
> --tezos-node http://127.0.0.1:8732 \
> --amount 2tz \
> id_strings.liq \
> --run main '"parameter"' '()'

The Liquidity usage page has more details. In short, we pass to the --run option the entry name (main), the parameter ("parameter"), and the storage (unit).

The output lists the contract name (Id_strings), the storage (unit), and the number of internal operations (0):

Main contract Id_strings
()
# Internal operations: 0

The output doesn’t complain which means the simulation ran successfully! This means the contract can be deployed to the testnet, which we’ll cover in the next part.

Run it on Tezos

Alternatively, we can run the compiled .tz script on the Tezos client directly:

$ tezos-client run script ./id_strings.tz on storage 'Unit' and input '"parameter"'
storage
Unit
emitted operations

Run it on the Liquidity online editor

Liquidity also provides an online editor. Copy and paste the code in the editor, click compile, and you’ll see the compiled Michelson code on the right. On the Test tab, click Run or Run step-by-step.

In the future, you can also compile your Liquidity contract to Michelson using LIGO. LIGO is another statically typed high-level smart-contract language that is under active development.

In the next tutorial, we’ll write a contract that does an operation, and deploy and inject it to the Tezos testnet! Stay tuned!