Liquidity Reference¶
Contract Format¶
All the contracts have the following form:
[%%version 2.1]
<... local declarations ...>
type storage = TYPE
let%init storage
(x : TYPE)
(y : TYPE)
... =
BODY
let%entry entrypoint1
(p1 : TYPE)
(s1 : TYPE) =
BODY
let%entry entrypoint2
(p2 : TYPE)
(s2 : TYPE) =
BODY
let%entry default
(parameter : TYPE)
(storage : TYPE) =
BODY
let%view view1
(parameter : TYPE)
(storage : TYPE)
: TYPE =
BODY
...
The optional version
statement tells the compiler in which version
of Liquidity the contract is written. The compiler will reject any
contract that has a version that it does not understand (too old, more
recent).
A contract is composed of type declarations, local values definitions,
an initializer, and a set of entry points and views. The type
storage
must be defined for all contracts.
Each entry point is a special function declared with the keyword
let%entry
. An entry point must have two arguments, the first one
being the parameter (whose type can be inferred, but we recommend
writing a type annotation for documentation purposes) and the second
one is the storage. The return type of the function can be specified
but is not necessary. Each entry point and view must be given a unique
name within the same contract.
If there is an entry point named default
, it will be the default
entry point for the contract, i.e. the one that is called when the
entry point is not specified in Contract.call
. It is generally a
good idea to make this entry point take a parameter of type unit, so
that the code will be executed by any transfer made to it without
arguments. (This can code to prevent accidental token transfers for
instance.)
An entry point always returns a pair (operations, storage)
, where
operations
is a list of internal operations to perform after
execution of the contract, and storage
is the final state of the
contract after the call. The type of the pair must match the type of a
pair where the first component is a list of opertations and the second
is the type of the storage argument.
Only with Love
Views are effect-less functions that return values (usually by reading the storage) and that are accessible outside the contract. Each view takes a parameter and a storage, and returns a value. We recommend writing the parameter and return types for documentation.
<... local declarations ...>
is an optional set of type, function
and extended primitives declarations. Type declarations can be used to
define records and variants (sum-types), described later in this
documentation.
An optional initial storage or storage initializer can be given with
let%init storage
. When deploying a Liquidity contract, if the
storage is not constant it is evaluated in the head context.
Note that types, values, entry points and values definitions can be written in any order as long as they are defined before their use (forward references are forbidden).
Basic Types and Values¶
Types in Liquidity are monomorphic. They are all inherited from Michelson, except for algebraic data types and records, that are translated to Michelson types.
Only with love Structured types are kept when compiling to Love.
Basic Types¶
The built-in base types are:
unit
: whose only constructor is()
bool
: Booleansint
: Unbounded integersnat
: Unbounded naturals (positive integers)dun
: The type of amountsstring
: character stringsbytes
: bytes sequencestimestamp
: dates and timestampskey
: cryptographic keyskey_hash
: hashes of cryptographic keyssignature
: cryptographic signaturesoperation
: type of operations, can only be constructedaddress
: abstract type of contract addresseschain_id
: abstract type for chain ids
Composite Types¶
Types can be composed using the following type operators:
- tuples: noted
t1 * t2
,t1 * t2 * t3
, etc. - functions:
'a -> 'b
is the type of functions from'a
to'b
, equivalent to('a, 'b) lambda
.
and the following predefined combinators:
- lists:
'a list
is the type of lists of elements in'a
- sets:
'a set
is the type of sets of elements in'a
('a
must be a comparable type) - maps:
('key, 'val) map
is the type of maps whose keys are of type'key
, a comparable type, and values of type'val
; - big maps:
('key, 'val) big_map
is the type of lazily deserialized maps whose keys are of type'key
(a comparable type) and values of type'val
; - contracts:
S.instance
is the type of contracts (instances) of signatureS
(see Contract Types and Signatures);
and the predefined algebraic data types:
- option type:
'a option = None | Some of 'a
- variant type:
('a, 'b) variant = Left of 'a | Right of 'b
Record and variant types must be declared beforehand and are referred to by their names.
User defined types can be parameterized by type variables. See Polymorphism for the specifics and limitations.
Contract Handles and View Types¶
Contracts can either be manipulated (stored, passed as argument to a function or a parameter to another contract) as addresses (this is a representation for an untyped contract, whose signature is unknown) or as typed values. Contracts can be freely converted to one type or the other (the internal representation during execution remains identical, but extra checks are performed when going from an address to a typed contract). These typed values mention only a subset of the contract signature. In fact they must mention only a single entry point or view in the signature, this is why we call them handles (to a specific entry point).
There are two kinds of types for these handles: handles to entry points and handles to views.
Handles to entry points need only mention the type of the parameter as such:
%[handle 'parameter]
Only with love Handles to views must mention the view name, the parameter type and the return type:
%[view (view_name : 'parameter -> 'return)]
These special types can be used anywhere a type is required.
Follow these links for the conversion primitives:
- Converting handles addresses :
Contract.address
. - Converting addresses to entry point handles:
[%handle: val%entry : ...]
or[%handle C.entry]
. - Only with love Converting addresses to view handles:
[%handle: val%view : ...]
or[%view C.view]
.
Constant Values¶
The unique constructor of type unit
is ()
.
The two Booleans (bool
) constants are:
true
false
As in Michelson, there are different types of integers:
int
: an unbounded integer, positive or negative, simply written0
,1
,2
,-1
,-2
, …nat
: an unbounded positive integer, written either with ap
suffix (0p
,12p
, etc.) or as an integer with a type coercion ((0 : nat)
).dun
: an unbounded positive float of DUNs, written either with aDUN
(ordun
) suffix (1.00DUN
, etc.) or as a string with type coercion (("1.00" : dun)
).
Strings (string
) are delimited by the characters "
and "
.
Bytes (bytes
) are sequences of hexadecimal pairs preceeded by 0x
, for
instance:
0x
0xabcdef
Timestamps (timestamp
) are written in ISO 8601 format, like in Michelson:
2015-12-01T10:01:00+01:00
Keys, key hashes and signatures are base58-check encoded, the same as in Michelson:
dn1HieGdCFcT8Lg9jDANfEGbJyt6arqEuSJb
is a key hash (key_hash
)edpkuit3FiCUhd6pmqf9ztUTdUs1isMTbF9RBGfwKk1ZrdTmeP9ypN
is a public key (key
)edsigedsigthTzJ8X7MPmNeEwybRAvdxS1pupqcM5Mk4uCuyZAe7uEk68YpuGDeViW8wSXMrCi5CwoNgqs8V2w8ayB5dMJzrYCHhD8C7
is a signature (signature
)
There are also three types of collections: lists, sets and maps. Constants collections can be created directly:
- Lists:
["x"; "y"]
for astring list
; - Sets:
Set [1; 2; 3; 4]
for anint set
; - Maps:
Map [1, "x"; 2, "y"; 3, "z"]
for a(int, string) map
; - Big maps:
BigMap [1, "x"; 2, "y"; 3, "z"]
for a(int, string) big_map
Options (option
) can be defined with:
- An empty option:
None
- A valued option:
Some 3
Variants (variant
) can be defined with:
- Left alternative:
Left "hello"
- Right alternative:
Right 3
for a (string, int) variant
.
The variant
type is not supposed to be used by programmers, who
can defined their own algebraic data types. Instead, variant
is
used when decompiling Michelson code.
It is also possible to coerce some constants between their inferred
type and another compatible type, using the notation
( CONSTANT : NEWTYPE )
:
- A
string
can be coerced todun
(the string must contain an integer in mudun à la Michelson),timestamp
,key
,address
,_ contract
,key_hash
andsignature
. - A
bytes
can be coerced toaddress
,_.instance
,key
,key_hash
andsignature
. - An constant
address
can be coerced to a contract handle. - A constant contract handle can be coerced to
address
. - A
key_hash
can be coerced to anaddress
and a contract handle (to entry pointdefault
of parameter typeunit
).
Starting with version 0.5
, constant values such as []
,
Map
, Set
, None
do not need to be annotated with their type
anymore. It will be inferred (when possible), see Type inference).
Pure (not closures) lambdas are also constants in Liquidity.
- For instance
fun (x : int) -> x + 1
can be used anywhere that a constant of typeint -> int
is required.
Predefined Primitives¶
There are two kinds of primitives in the language:
- Prefix primitives are used by putting the primitive before the
arguments:
prim x y z
. All alphanumerical primitives are prefix primitives, exceptlor
,lxor
,mod
,land
,lsl
,lsr
andasr
. - Infix primitves are used by putting the primitive between the
arguments:
x prim y
. Infix primitives are always operators (+
,-
, etc.).
When the type of a primitive is specified, we extend the notation for functions like this:
TYPE_ARG -> TYPE_RESULT
for a primitive with one argumentTYPE_ARG1 -> TYPE_ARG2 -> TYPE_RESULT
for a primitive with two arguments
Extended Primitives¶
Only with michelson
Additional prefix Michelson primitives can be added to the language through a local declaration as follows:
external prim_name : TYPE1 -> ... -> TYPE_ARG1 -> ... -> TYPE_RESULT = "MINST" FLAGS
Such declaration takes as input an arbitrary number of type arguments
(TYPE1 -> ...
) of the form [%type: 'a]
, where 'a
is the
variable bound to the type.
Then follows an arbitrary (but non-null) number of typed arguments
(TYPE_ARG1 -> ...
) of the form [%stack: TYPE]
, where TYPE
corresponds to any Michelson type, possibly containing one or more of
the type variables introduced previously. Here, %stack
means the
argument resides on the stack. It is mandatory for all arguments,
except when declaring a primitive that takes no argument, in which
case it takes a single argument of type unit
, without the
%stack
specifier ([%stack: unit]
would instead mean
that the primitive takes a unit value from the stack).
The result type (TYPE_RESULT
) is specified using the same form as
arguments, i.e. [%stack: TYPE]
, where a bare unit
indicates
a primitive that does not produce any value on the stack. It is
also possible to specify that the primitive returns several
values on the stack using a tuple notation :
[%stack: TYPE1] * [%stack: TYPE2] * ...
. In this case, every
component of the tuple must have a %stack
specifier and will
occupy a different stack cell. All the values will be assembled
into an actual tuple before being returned to Liquidity.
MINST
is the actual Michelson instruction to generate and will
be written as-is in the output file, followed by the given type
arguments, if any.
FLAGS
allows to give additional information about the primitive.
Currently, the only supported flag is [@@effect]
, which specifies
that the primitive may have side-effects. This prevents calls to
this primitive from being inlined or eliminated when the return
value is not used.
A call to an extended primitive is then performed as follows:
prim_name TYPE1 ... ARG1 ...
After the primitive name, a number of type arguments (TYPE1 ...
)
of the form [%type: TYPE]
may be given (if the primitive has
been declared to take type arguments), where TYPE
is any
Michelson type. Then follow the actual arguments (ARG1 ...
).
Comparison between values¶
All values are not comparable. Only two values of the following types can be compared with each other:
bool
int
nat
dun
string
bytes
timestamp
key_hash
address
The following comparison operators are available:
=
: equal<>
: not-equal<
: strictly less<=
: less or equal>
: strictly greater>=
: greater or equal
There is also a function compare : 'a -> 'a -> int
to compare two
values and return an integer, as follows. compare x y
- returns 0 if
x
andy
are equal - returns a strictly positive integer if
x > y
- returns a strictly negative integer if
x < y
The Current
module¶
Try onlineCurrent.balance: unit -> dun
: returns the balance of the current contract. The balance contains the amount of dun that was sent by the current operation.type storage = dun let%entry default () s = let bal = Current.balance() in [], bal
Try onlineCurrent.time: unit -> timestamp
: returns the timestamp of the block in which the transaction is included. This value is chosen by the baker that is including the transaction, so it should not be used as a reliable source of alea.type storage = timestamp let%entry default () _ = let now = Current.time() in [], now
Try onlineCurrent.amount: unit -> dun
: returns the amount of dun transferred by the current operation (standard or internal transaction).type storage = dun let%entry default () _ = let received = Current.amount () in [], received
Try onlineCurrent.source: unit -> address
: returns the address that initiated the current top-level transaction in the blockchain. It is the same one for all the transactions resulting from the top-level transaction, standard and internal. It is the address that paid the fees and storage cost, and signed the operation on the blockchain.type storage = address let%entry default () owner = let addr = Current.source () in if addr <> owner then Current.failwith ("Not allowed"); [], owner
Try onlineCurrent.sender: unit -> address
: returns the address that initiated the current transaction. It is the same as the source for the top-level transaction, but it is the originating contract for internal operations.type storage = address let%entry default () owner = let addr = Current.sender () in if addr <> owner then Current.failwith ("Sender cannot call"); [], owner
Try onlinefailwith
orCurrent.failwith: 'a -> 'b
: makes the current transaction and all its internal transactions fail. No modification is done to the context. The argument can be any value (often a string and some argument), the system will display it to explain why the transaction failed.type storage = unit let%entry default (param : string) _ = if String.length param > 256p then Current.failwith ("Parameter too long", param); [], ()
Try onlineCurrent.block_level: unit -> nat
: returns the level of the block in which the transaction is included.type storage = nat let%entry default () start_level = if Current.block_level () < start_level then failwith "not started"; [], start_level
Try onlineCurrent.collect_call: unit -> bool
: returnstrue
if the current call is a collect call..type storage = unit let%entry default () () = if Current.collect_call () then failwith "Cannot be called in a collect call"; [], ()
Operations on tuples¶
Try onlineget t n
,Array.get t n
andt.(n)
wheren
is a constant positive-or-nul int: returns then
-th element of the tuplet
. Tuples are translated to Michelson by pairing on the right, i.e.(a,b,c,d)
becomes(a, (b, (c, d)))
. In this example,a
is the0
-th element.type storage = unit let%entry default () _ = let x = (1, 2, 3, 4) in let car = x.(0) in let cdr = x.(1) in if car <> 1 || cdr <> 2 then failwith "Error !"; [], ()
Try onlineset t n x
,Array.set t n x
andt.(n) <- x
wheren
is constant positive-or-nul int: returns the tuple where then
-th element has been replaced byx
.type storage = unit let%entry default () _ = let x = (1,2,3,4) in let x0 = x.(0) <- 10 in let x1 = x0.(1) <- 11 in if x1.(0) <> 10 || x1.(1) <> 11 || x1.(2) <> 3 || x1.(3) <> 4 then failwith "Error !"; [], ()
Operations on numeric values¶
+
: Addition. With the following types:dun -> dun -> dun
nat -> nat -> nat
int|nat -> int|nat -> int
timestamp -> int -> timestamp
int -> timestamp -> timestamp
-
: Substraction. With the following types:dun -> dun -> dun
int|nat -> int|nat -> int
timestamp -> int -> timestamp
timestamp -> timestamp -> int
int|nat -> int
(unary negation)
*
: Multiplication. With the following types:nat -> dun -> dun
dun -> nat -> dun
nat -> nat -> nat
Try onlinenat|int -> nat|int -> int
type storage = dun let%entry default ( v : nat ) _ = (* conversion from nat to dun *) let amount = v * 1DUN in [], amount
/
: Euclidian division. With the following types:nat -> nat -> ( nat * nat ) option
int|nat -> int|nat -> ( int * nat ) option
dun -> nat -> ( dun * dun ) option
Try onlinedun -> dun -> ( nat * dun ) option
type storage = nat let%entry default ( v : dun ) _ = (* conversion from dun to nat *) let (nat, rem_dun) = match v / 1DUN with | Some qr -> qr | None -> failwith "division by 0 impossible" in [], nat
~-
: Negation. Type:int|nat -> int
lor
,or
and||
: logical OR with the following types:bool -> bool -> bool
nat -> nat -> nat
&
,land
and&&
: logical AND with the following types:bool -> bool -> bool
nat|int -> nat -> nat
lxor
,xor
: logical exclusive OR with the following types:bool -> bool -> bool
nat -> nat -> nat
not
: logical NOTbool -> bool
nat|int -> int
(two-complement with sign negation)
abs
: Absolute value. Typeint -> int
is_nat
: Maybe positive. Typeint -> nat option
.Instead of using
Try onlineis_nat
, it is recommended to use a specific form of pattern matching:type storage = nat let%entry default ( x : int ) _ = (* conversion from int to nat *) let n = match%nat x with | Plus n -> n | Minus _ -> failwith "x shound not be negative" in [], n
int
: To integer. Typenat -> int
>>
andlsr
: Logical shift right. Typenat -> nat -> nat
<<
andlsl
: Logical shift left. Typenat -> nat -> nat
Operations on contracts¶
Try onlineContract.call: dest:(address | [%handle 'a] | 'S.instance) -> amount:dun -> ?entry:<entry_name> -> parameter:'a -> operation
. Forge an internal contract call. Arguments can be labeled, in which case they can be given in any order. The entry point name is optional (default
by default). The destination is either a contract handle to an entry point, a contract instance, or an address (in the last two cases, an entry point must be specified).type storage = unit let%entry default ( to_forward : dun ) _ = let op = Contract.call ~dest:(dn1UqnHgHFe8ezEgsoow4hERctPssuWiw9h8 : address) ~entry:default ~amount:to_forward () in [op], ()
Try online<c.entry>: 'parameter -> amount:dun -> operation
. Forge an internal contract call. The amount argument can be labeled, in which case it can appear before the parameter.c
is either a contract handle (of type[%handle 'parameter]
) or an address.contract type My = sig val%entry my_entry : int end type storage = unit let%entry default ((amount : dun ), (p : int), (c : address)) _ = let op1 = c.my_entry p ~amount in (* this is syntactic sugar for: *) let op2 = Contract.call ~dest:c ~entry:my_entry ~parameter:p ~amount in [op1; op2], ()
Try onlineAccount.transfer: dest:key_hash -> amount:dun -> operation
. Forge an internal transaction to the implicit (_i.e._ default) account contract ofdest
. Arguments can be labeled, in which case they can be given in any order. The resulting operation cannot fail (if the transfer amount leaves more than 0.257DUN on both contracts).type storage = unit let%entry default () _ = let op = Account.transfer ~dest:dn1UqnHgHFe8ezEgsoow4hERctPssuWiw9h8 ~amount:1DUN in [op], ()
Try onlineContract.view: dest:(address | [%view (view_name : 'a -> 'b)]) -> ?view:<view_name> -> parameter:'a -> 'b
. Call a specific contract view. Arguments can be labeled, in which case they can be given in any order. The view name is mandatory if andest
is an address (it is not required ifdest
is a view handle). The destination is either a view handle or an address (in which case, an view name point must be specified). Only with lovetype storage = (int, bool) map let%entry default (c : [%view (get_i : bool -> int)]) s = if Contract.view c get_i false > 0 then failwith (); [], s
Try onlineAccount.default: key_hash -> [%handle unit]
. Returns a contract handle to thedefault
entry point of the implicit account associated to the givenkey_hash
. Transfers to it cannot fail.type storage = address option let%entry default (k : key_hash) _ = let my_contract = Account.default k in let op = my_contract.default () ~amount:0DUN in [op], Some (Contract.address my_contract)
Try onlineContract.set_delegate: key_hash option -> operation
. Forge a delegation operation for the current contract. ANone
argument means that the contract should have no delegate (it falls back to its manager). The delegation operation will only be executed in an internal operation if it is returned at the end of the entry point definition.type storage = unit let%entry default () () = (* accept funds *) [], () let%entry change_delegate (new_del : key_hash) () = let op = Contract.set_delegate (Some new_del) in [op], () let%entry remove_delegate () () = let op = Contract.set_delegate None in [op], ()
Try onlineContract.address: [%handle 'a] -> address
. Returns the address of a contract. The returned address can be converted to any entry point (or view) handle of the contract (contrary toContract.untype
).type storage = { x : int; my_address : address; } let%entry default () storage = let addr = Contract.address (Contract.self ()) in let storage = storage.my_address <- addr in [], storage
Try onlineContract.untype: [%handle 'a] -> address
. Returns the address corresponding to an untype version of the contract handle.type storage = { x : int; c : address; } let%entry default () storage = let addr = Contract.untype (Contract.self ()) in let storage = storage.c <- addr in [], storage
Try onlineC.at: address -> C.instance option
. Returns the contract associated with the address and of typeC
, if any. Only with lovelet%entry default2 (addr : address) _ = begin match BoolContract.at addr with | None -> failwith ("Cannot recover bool contract from:", addr) | Some _c -> () end; [], ()
Try online[%handle: val%entry <entry_name> : 'a ] : address -> [%handle 'a] option
. Returns a contract handle to the entry point<entry_name>
if the contract at the specified address has an entry point named<entry_name>
of parameter type'a
. If no such entry point exists or the parameter type is different then this function returnsNone
. For any contract or contract typeC
, you can also use the syntactic sugar[%handle C.<entry_name>]
instead.type storage = unit contract type BoolContract = sig val%entry default : bool end let%entry default (addr : address) _ = begin match [%handle BoolContract.default] addr with | None -> failwith ("Cannot recover bool contract from:", addr) | Some _my_handle -> () end; [], ()
Try online[%handle: val%view <view_name> : 'a -> 'b] : address -> [%view (view_name : 'a -> 'b)] option
. Returns a contract view handle to the view<view_name>
if the contract at the specified address has an view named<view_name>
of parameter type'a
and return type'b
. If no such view exists or the parameter or return types are different then this function returnsNone
. For any contract or contract typeC
, you can also use the syntactic sugar[%view C.<view_name>]
instead. Only with lovelet%entry default2 (c : address) s = match [%view C0.get_i] c with | None -> failwith () | Some c -> if Contract.view c get_i () > 0 then failwith (); [], s
Try onlineContract.get_balance: [%handle 'a] -> dun
. Returns the balance of the contract.type storage = unit let%entry default addr () = match [%handle: val%entry default : unit] addr with | None -> failwith () | Some c -> if Current.balance () < Contract.get_balance c then failwith "balance too big"; [], ()
Try onlineContract.is_implicit: [%handle unit] -> key_hash option
. Returns the key hash of a contract handle if it is an implicit one, otherwise, returnsNone
.type storage = key_hash let%entry default () _ = match [%handle: val%entry default : unit] (Current.sender ()) with | None -> failwith "can only be called by implicit contract" | Some c -> match Contract.is_implicit c with | None -> failwith "can only be called by implicit contract" | Some kh -> [], kh
Try online[%handle Self.<entry>] -> [%handle 'a]
. Returns a handle to the entry point<entry>
of the currently executing contract. You can use the syntactic sugarContract.self ()
for[%handle Self.default]
.type storage = unit let%entry default () _ = let me = [%handle Self.other] in let op = me.other 10 ~amount:0DUN in [op], () let%entry other (x : int) _ = if x < 0 then failwith (); [], ()
Try onlineContract.create: delegate:key_hash option -> amount:dun -> storage:'storage -> code:(contract _) -> (operation, address)
. Forge an operation to originate a contract with code. The contract is only created when the operation is executed, so it must be returned by the transaction. Note that the code must be specified as a contract structure (inlined or not).Contract.create delegate_opt initial_amount initial_storage (contract C)
forges an an origination operation for contractC
with optional delegatedelegate
, initial balanceinitial_amount
and initial storageinitial_storage
. Arguments can be named and put in any order.type storage = address let%entry default (delegate : key_hash) _ = let initial_storage = (10DUN, "Hello") in let (op, addr) = Contract.create ~storage:initial_storage ~delegate:(Some delegate) ~amount:10DUN (contract struct type storage = dun * string let%entry default () s = [], s end) in [op], addr
The contract code parameter is a first class value, it can be written inlined as above, or equivalently the contract code can be referred to by its name (in scope) as below:
Try onlinetype storage = address contract S = struct type storage = dun * string let%entry default () s = [], s end let%entry default (delegate : key_hash) _ = let initial_storage = (10DUN,"Hello") in let (op, addr) = Contract.create ~storage:initial_storage ~delegate:(Some delegate) ~amount:10DUN (contract S) in [op], addr
Cryptographic operations¶
Try onlineCrypto.blake2b: bytes -> bytes
. Computes the cryptographic hash of a bytes with the cryptographic Blake2b function.type storage = bytes let%entry default () _ = let b = 0xdeadbeef in let h = Crypto.blake2b b in if Bytes.length h <> 32p then failwith "incorrect size"; if h <> 0xf3e925002fed7cc0ded46842569eb5c90c910c091d8d04a1bdf96e0db719fd91 then failwith "incorrect hash"; [], h
Try onlineCrypto.sha256: bytes -> bytes
. Computes the cryptographic hash of a bytes with the cryptographic Sha256 function.type storage = bytes let%entry default () _ = let b = Bytes.pack "This is a message" in let h = Crypto.sha512 b in if Bytes.length h <> 32p then failwith "incorrect size"; if h <> 0x8624d6634774f992f349961d6991f57b6b437e2a48aebafcca03f14e29252f5e then failwith "incorrect hash"; [], h
Try onlineCrypto.sha512: bytes -> bytes
. Computes the cryptographic hash of a bytes with the cryptographic Sha512 function.type storage = bytes let%entry default () _ = let b = Bytes.pack [1; 2; 3] in let h = Crypto.sha512 b in if Bytes.length h <> 64p then failwith "incorrect size"; if h <> 0x97f36bcf0a1d65c0d49852a56d93f3b1b15712a94e251ad88a619b2db7bfa34b85e3a7fc8dff5254bf0eacad4d979430cb1f12a7b094ecf295020597f9de7254 then failwith "incorrect hash"; [], h
Try onlineCrypto.hash_key: key -> key_hash
. Hash a public key and encode the hash in B58check.type storage = key_hash let%entry default (k : key) _ = let h = Crypto.hash_key k in [], h
Try onlineCrypto.check: key -> signature -> bytes -> bool
. Check that the signature corresponds to signing the (Blake2b hash of the) sequence of bytes with the public key. Signatures generated bydune-client sign bytes ...
can be checked this way.type storage = key let%entry default ((message : string), (signature : signature)) key = let bytes = Bytes.pack message in if not (Crypto.check key signature bytes) then failwith "Wrong signature"; [], key
Operations on bytes¶
Try onlineBytes.pack: 'a -> bytes
. Serialize any data to a binary representation in a sequence of bytes.type storage = unit let%entry default () _ = let b = Bytes.pack [1; 2; 3; 4; 5] in let hash = Crypto.sha256 b in if hash = 0x then failwith "?"; [], ()
Try onlineBytes.unpack: bytes -> 'a option
. Deserialize a sequence of bytes to a value from which it was serialized. The expression must be annotated with the (option) type that it should return.type storage = unit let%entry default () _ = let s = Bytes.pack (1, 2, 3, 4) in let t = (Bytes.unpack s : (int * int * int * int) option) in begin match t with | None -> failwith "bad unpack" | Some t -> if t.(0) <> 1 then failwith "bad unpack" end; [], ()
Try onlineBytes.length
orBytes.size: bytes -> nat
. Return the size of the sequence of bytes.type storage = unit let%entry default () _ = let s = Bytes.pack (1, 2, 3, 4) in let n = Bytes.length s in if n > 16p then failwith "serialization too long"; [], ()
Try onlineBytes.concat: bytes list -> bytes
. Append all the sequences of bytes of a list into a single sequence of bytes.type storage = unit let%entry default () _ = let s = Bytes.concat [ 0x616161; 0x616161 ] in if Bytes.length s <> 6p then failwith "bad concat !"; [], ()
Try onlineBytes.slice
orBytes.sub" of type ``nat -> nat -> bytes -> bytes option
. Extract a sequence of bytes within another sequence of bytes.Bytes.slice start len b
extracts the bytes subsequence ofb
starting at indexstart
and of lengthlen
. A return valueNone
means that the position or length was invalid.type storage = unit let%entry default () _ = let b = 0x616161 in let s = Bytes.concat [ b; b ] in let b' = Bytes.sub 3p 3p s in begin match b' with | None -> failwith "Bad concat or sub !" | Some b' -> if b <> b' then failwith "Bad concat or sub !"; end; [], ()
Try online( @ ) : bytes -> bytes -> bytes
. Append two sequences of bytes into a single sequence of bytes.b1 @ b2
is syntactic sugar forBytes.concat [b1; b2]
.type storage = bytes let%entry default () _ = let b = 0x616161 in let s = b @ b in let b' = match Bytes.sub 3p 3p s with | Some b -> b | None -> failwith () in [], b'
Operations on strings¶
A string is a fixed sequence of characters. They are restricted to the
printable subset of 7-bit ASCII, plus some escaped characters (\n
,
\t
, \b
, \r
, \\
, \"
).
Try onlineString.length
orString.size
of typestring -> nat
. Return the size of the string in characters.type storage = nat let%entry default () _ = let s = "Hello world" in let len = String.length s in [], len
Try onlineString.slice
orString.sub
with typenat -> nat -> string -> string option
.String.sub start len s
returns a substring of a strings
at the given starting at positionlen
with the specified lengthlen
, orNone
if invalid.type storage = string let%entry default () _ = let s = "Hello world" in let world = match String.sub 6p 5p s with | Some s -> s | None -> failwith () in [], world
Try onlineString.concat: string list -> string
. Append all strings of a list into a single string.type storage = unit let%entry default () _ = let s1 = "Hello world" in let s2 = String.concat [ "Hello"; " "; "world" ] in if s1 <> s2 then failwith (s1, s2); [], ()
Try online( @ ) : string -> string -> string
. Append two strings into a single string.s1 @ s2
is syntactic sugar forString.concat [s1; s2]
.type storage = unit let%entry default () _ = let s1 = "Hello world" in let s2 = "Hello " @ "world" in if s1 <> s2 then failwith (s1, s2); [], ()
Operations on lambdas¶
Lambda.pipe
or( |> )
of type'a -> ('a -> 'b) -> 'b
or'a -> ('a,'b) closure -> 'b
. Applies a function or closure to its argument.
Try online( @@ ) : ('a -> 'b) -> 'a -> 'b
is also function application.type storage = unit let%entry default () _ = let square x = x * x in let x = 23 |> square in let y = square 23 in (* this is the same as x *) let z = square @@ 23 in (* this is also the same as x *) if x <> y || x <> z then failwith (x, y, z); [], ()
Operations on lists¶
Lists are immutable data structures containing values (of any type) that can only be accessed in a sequential order. Since they are immutable, all modification primitives return a new list, and the list given in argument is unmodified.
Try online( :: ) : 'a -> 'a list -> 'a list
Add a new element at the head of the list. The previous list becomes the tail of the new list.type storage = string list let%entry default () old_list = let new_list = "Hello" :: old_list in [], new_list
Try onlineList.rev : 'a list -> 'a list
Return the list in the reverse order.type storage = unit let%entry default () _ = let list = List.rev [7; 5; 10] in (* list = [10; 5; 7] *) begin match list with | x :: _ -> if x <> 10 then failwith () | _ -> () end; [], ()
Try onlineList.length
orList.size: 'a list -> nat
. Return the length of the list.type storage = unit let%entry default () _ = let size = List.length [10; 20; 30; 40] in if size <> 4p then failwith size; [], ()
Try onlineList.iter: ('a -> unit) -> 'a list -> unit
. Iter the function on all the elements of a list. Since no value can be returned, it can only be used for side effects, i.e. to fail the transaction.type storage = unit let%entry default (list : nat list) _ = List.iter (fun x -> if x < 10p then failwith "error, element two small" ) list; [], ()
Try onlineList.fold: ('elt * 'acc -> 'acc) -> 'elt list -> 'acc -> 'acc
. Iter on all elements of a list, while modifying an accumulator.type storage = unit let%entry default () _ = let sum = List.fold (fun (elt, acc) -> elt + acc ) [1; 2; 3; 4; 5] 0 in if sum <> 15 then failwith sum; [], ()
Try onlineList.map: ('a -> 'b) -> 'a list -> 'b list
. Return a list with the result of applying the function on each element of the list.type storage = int list let%entry default () list = let list = List.map (fun x -> x + 1) list in [], list
Try onlineList.map_fold: ('a * 'acc -> 'b * 'acc) -> 'a list -> 'acc -> 'b list * 'acc
. Return a list with the result of applying the function on each element of the list, plus an accumulator.type storage = int let%entry default () _ = let (list, acc) = List.map_fold (fun (elt, acc) -> ( elt + 1, elt + acc ) ) [1; 2; 3; 4; 5] 0 in [], acc
Operations on sets¶
Sets are immutable data structures containing unique values (a comparable type). Since they are immutable, all modification primitives return a new updated set, and the set given in argument is unmodified.
Try onlineSet.update: 'a -> bool -> 'a set -> 'a set
. Update a set for a particular element. If the boolean istrue
, the element is added. If the boolean isfalse
, the element is removed.type storage = int set let%entry default () my_set = let my_set = Set.update 3 true my_set in (* add 3 *) let my_set = Set.update 10 false my_set in (* remove 10 *) [], my_set
Try onlineSet.add: 'a -> 'a set -> 'a set
. Add an element to a set, if not present.Set.add x s
is syntactic sugar forSet.update x true s
.type storage = int set let%entry default () my_set = let my_set = Set.add 3 my_set in [], my_set
Try onlineSet.remove: 'a -> 'a set -> 'a set
. Remove an element to a set, if present.Set.remove x s
is syntactic sugar forSet.update x false s
.type storage = int set let%entry default () my_set = let my_set = Set.remove 10 my_set in [], my_set
Try onlineSet.mem: 'a -> 'a set -> bool
. Returntrue
if the element is in the set,false
otherwise.type storage = int set let%entry default () my_set = let my_set = Set.add 3 my_set in if not ( Set.mem 3 my_set ) then failwith "Missing integer 3 in int set"; [], my_set
Try onlineSet.cardinal
orSet.size
with type'a set -> nat
. Return the number of elements in the set.type storage = unit let%entry default (my_set : int set) _ = let cardinal = Set.size my_set in if cardinal < 10p then failwith "too few elements"; [], ()
Try onlineSet.iter: ('ele -> unit) -> 'ele set -> unit
. Apply a function on all elements of the set. Since no value can be returned, it can only be used for side effects, i.e. to fail the transaction.type storage = unit let%entry default (my_set : int set) _ = Set.iter (fun ele -> if ele < 0 then failwith "negative integer") my_set; [], ()
Operations on maps¶
Maps are immutable data structures containing associations between keys (a comparable type) and values (any type). Since they are immutable, all modification primitives return a new updated map, and the map given in argument is unmodified.
Try onlineMap.add: 'key -> 'val -> ('key, 'val) map -> ('key, 'val) map
. Return a map with a new association between a key and a value. If an association previously existed for the same key, it is not present in the new map.type storage = (int, string) map let%entry default () map = let map = Map.add 1 "Hello" map in let map = Map.add 2 "World" map in [], map
Try onlineMap.remove: 'key -> ('key,'val) map -> ('key,'val) map
. Return a map where any associated with the key has been removed.type storage = (int, string) map let%entry default (id : int) map = let map = Map.remove id map in [], map
Try onlineMap.find: 'key -> ('key,'val) map -> 'val option
. Return the value associated with a key in the map.type storage = (int, string) map let%entry default (id : int) map = let _v = match Map.find id map with | None -> failwith ("id is not in the map", id) | Some v -> v in [], map
Try onlineMap.update: 'key -> 'val option -> ('key,'val) map -> ('key,'val) map
. Return a new map where the association between the key and the value has been removed (caseNone
) or added/updated (caseSome v
).type storage = (int, string) map let%entry default ((id : int), (v : string)) map = let new_map = Map.update id None map in (* removed *) let new_map = Map.update id (Some v) new_map in (* added *) [], new_map
Try onlineMap.mem: 'key -> ('key, 'val) map -> bool
. Returntrue
if an association exists in the map for the key,false
otherwise.type storage = (address, string) map let%entry default () owners_map = let sender = Current.sender () in if not ( Map.mem sender owners_map ) then failwith ("not allowed", sender); [], owners_map
Try onlineMap.cardinal
orMap.size
with type('key,'val) map -> nat
. Return the number of associations (i.e. uniq keys) in the map.type storage = (address, string) map let%entry default () owners_map = if Map.size owners_map = 0p then failwith "no owners"; [], owners_map
Try onlineMap.iter: ('key * 'val -> unit) -> ('key,'val) map -> unit
. Apply a function on all associations in the map. Since no value can be returned, it can only be used for side effects, i.e. to fail the transaction.type storage = (string, int) map let%entry default () map = Map.iter (fun (_, v) -> if v < 0 then failwith "No option should be negative" ) map; [], map
Try onlineMap.fold: (('key * 'val) * 'acc -> 'acc) -> ('key,'val) map -> 'acc -> 'acc
. Apply a function on all associations of the map, updating and returning an accumulator.type storage = (string, int) map let%entry default () map = let sum_vals = Map.fold (fun ((_, v), acc) -> acc + v) map 0 in if sum_vals <= 0 then failwith "Need at least one positive"; [], map
Try onlineMap.map: ('key * 'src -> 'dst) -> ('key,'src) map -> ('key,'dst) map
. Apply a function on all associations of a map, and return a new map where keys are now associated with the return values of the function.type storage = (string, int) map let%entry default () map = let negated_map = Map.map (fun (_key, v) -> - v) map in [], negated_map
Try onlineMap.map_fold: (('key * 'src) * 'acc -> 'dst * 'acc) -> ('key,'src) map -> 'acc -> ('key,'dst) map * 'acc
. Apply a function on all associations of a map, returning both a new map and an updated accumulator.type storage = (string, int) map let%entry default () map = let negated_values, min_key = Map.map_fold (fun ((key, v) , min_key) -> let min_key = match min_key with | None -> Some key | Some min -> if key < min then Some key else min_key in ( - v, min_key ) ) map None in [], negated_values
Operations on Big maps¶
Big maps are a specific kind of maps, optimized for storing. They can be updated incrementally and scale to a high number of associations, whereas standard maps will have an expensive serialization and deserialization cost. Big maps cannot be iterated and cannot have big maps as their keys or as their elements.
Try onlineMap.find: 'key -> ('key,'val) big_map -> 'val option
. Return the value associated with a key in the map.type storage = { big : (int, string) big_map; nothing : unit } let%entry default (param : int) storage = let _v = match Map.find param storage.big with | None -> failwith ("param is not in the map", param) | Some v -> v in [], storage
Try onlineMap.mem: 'key -> ('key, 'val) big_map -> bool
. Returntrue
if an association exists in the map for the key,false
otherwise.type storage = { big : (int, string) big_map; nothing : unit } let%entry default (param : int) storage = if not (Map.mem param storage.big) then failwith ("param is not in the map", param); [], storage
Map.update: 'key -> 'val option -> ('key,'val) big_map -> ('key,'val) big_map
. Return a new map where the association between the key and the value has been removed (caseNone
) or added/updated (caseSome v
).Map.add: 'key -> 'val -> ('key, 'val) big_map -> ('key, 'val) big_map
. Syntactic sugar forMap.update (Some ...)
.
Try onlineMap.remove: 'key -> ('key,'val) big_map -> ('key,'val) big_map
. Syntactic sugar forMap.update None
.type storage = { big : (int, string) big_map; nothing : unit } let%entry default () storage = let big = Map.add 10 "ten" storage.big in let big = Map.remove 0 big in let big = Map.update 0 (Some "zero") big in let big = Map.update 1 None big in let storage = storage.big <- big in [], storage
Operations on generic collections¶
These primitives should not be used directly in Liquidity. They are
only used by the decompiler. They are automatically replaced during
typing by the corresponding primitive for the collection of the
argument (in either List
, Set
, Map
, String
or
Bytes
). However, they can be used to write some polymorphic code on
collections.
Coll.update
Coll.mem
Coll.find
Coll.size
Coll.concat
Coll.slice
Coll.iter
Coll.fold
Coll.map
Coll.map_fold
The Modules and Contracts System¶
The system described in this section allows to define several contracts and modules in the same file, to reference contracts by their names, and to call contracts defined in other files.
The notion of contract and module structures in Liquidity is a way
to define namespaces and to encapsulate types, values and contracts in
packages. These packages are called structures and are introduced with
the struct
keyword. Modules, introduced with the keyword
module
, can contain types and values but cannot contain any entry
points. Contracts are introduced with the keyword contract
, they
can contain types, values and must have at least one entry point.
Types in scope (defined before their use) can be referred to anywhere,
provided they are adequately qualified (with a dot .
notation).
Values are exported outside the module or the contract by default,
which means they can be used by other modules and contracts. One can
annotate the value with [@private]
to prevent exporting the value.
For instance the following example defines a module M
with a type
t
and an exported function f
.
module M = struct
type t = int
let f (x : int) = x + 1
end
The contract C
can be defined as such. It uses the type t
of
M
, written M.t
and the function f
of M
written
M.f
. The function succ
is exported and can be called with
C.succ
outside the contract, whereas prev
cannot (the compiler
will complain that is does not know the symbol C.prev
if we try to
use it elsewhere).
contract C = struct
type storage = M.t
let%init storage = 0
let succ x = M.f x [@@inline]
let[@private] prev x = x + 1 [@@inline]
let%entry default () storage =
[], prev (succ storage)
end
The toplevel contract can use elements from either structures. Here we
use types and functions from both M
and C
and call the entry
point default
of a contract instance of type C
.
type storage = M.t
let%entry default (c : address) s =
[c.main () ~amount:0DUN], C.succ (M.f (2 * s))
Module and Contract Aliases¶
Modules and contracts can be arbitrarily nested and aliases can be defined by simply giving the qualified name (instead of the whole structure).
Try onlinemodule M2 = struct
type t = bool
module MI = struct
type r = t
let m_and (x, y) : bool = x && y
end
end
module MI_alias = M2.MI
contract C_alias = C
First Class Contract Structures¶
Contracts structures (note we are not talking about contract instances here) can also be used as first class values:
Try onlinetype storage = address
contract S = struct
type storage = dun * string
let%entry default () s = [], s
end
let%entry default (delegate : key_hash) _ =
let initial_storage = (10DUN,"Hello") in
let (op, addr) =
Contract.create
~storage:initial_storage ~delegate:(Some delegate) ~amount:10DUN
(contract S) in
[op], addr
Handles to contracts can be called with three different syntaxes:
Contract.call ~dest:c ~amount:1DUN ~parameter:"hello"
Contract.call ~dest:c ~amount:1DUN ~entry:default ~parameter:"hello"
c.default "hello" ~amount:1DUN
These calls are all equivalent when c is an address or a handle to the default entry point.
Toplevel Contracts¶
A contract defined at toplevel in a file path/to/my_contract.liq
implicitly defines a contract structure named My_contract
which
can be called in other Liquidity files.
Contract Types and Signatures¶
A contract is a first class object in Liquidity only for the
instruction Contract.create
, while contract handles can be
used like any other values. Contract signatures are introduced with
the keyword sig
and defined with the keyword contract type
:
contract type S = sig
type t1 = ...
type t2 = ...
val%entry entry1 : TYPE
val%entry entry2 : TYPE
val%entry default : TYPE
val%view view1 : TYPE -> TYPE
val%view view2 : TYPE -> TYPE
...
end
A contract signature can contain:
- type declarations,
- declarations for the entry point signatures with the special keyword
val%entry
in which only the type parameter must be specified, - declarations for the view signatures with the special keyword
val%view
whose type must be an arrow type of the parameter to the result type.
Only with Love
The type of a contract (instance) whose signature is C is written
C.instance
. Note that C
must be declared as a contract or a
contract signature beforehand if we want to declare values of type
C.instance
.
For example:
type t = {
counter : int;
dest : C.instance;
}
is a record type with a contract field dest
of signature C
.
Predefined Contract Signatures¶
The contract signature UnitContract
is built-in, in Liquidity, and
stands for contracts with a single entry point default
whose
parameter is of type unit
:
contract type UnitContract = sig
type storage
val%entry default : unit
end
type storage = unit
let%entry default (c : address) _ =
match [%handle UnitContract.default] c with
| None -> failwith ()
| Some c -> [c.default ~amount:0DUN ()], ()
Type inference and Polymorphism¶
A new addition of version 0.5
of the Liquidity compiler is a type
inference algorithm (a variant of Hindley-Milner type inference) which
works in the presence of parametric types and polymorphic values
(functions) and can infer parametric types.
Type inference¶
A consequence of this addition is that most type annotations in Liquidity are now unnecessary, but can be used to restrict types or to enforce a constraint. This makes programs more readable by removing superfluous noise.
In particular, types of entry point parameters, storage initializer
parameters, constant values (like []
, None
, etc.) and
functions are not necessary anymore.
The following example shows type inference at work.
Try onlinetype storage = unit
type t = { x : int; y : nat }
(* type of bool_to_int is inferred to: bool -> int *)
let bool_to_int c =
if c then 1 else 0
(* type of pos is inferred to: int -> bool *)
let pos i = i > 0
let%entry default param _ =
(* type of l is inferred to: (dun, int) variant *)
let l = Left 1DUN in
begin match l with
| Right x -> if x > 0 then failwith ()
| Left _ -> ()
end;
(* type of param is inferred to: t *)
if not (pos param.x) then failwith ();
let v_packed = Bytes.pack (0DUN, 6, 88) in
let v_unpacked = Bytes.unpack v_packed in
begin match v_unpacked with
| None -> failwith ()
| Some (x, y, z) ->
if x <> 0DUN || (z <> 0 && y = z) then failwith ()
end;
(* type of v_unpacked is infered to: (dun * int * int) *)
(* type of [] is inferred to: operation list *)
([], ())
Polymorphism¶
In general, values in Liquidity cannot be polymorphic: type variables
must (and will) be instantiated (by inference and
monomorphization). This restriction is inherited from
Michelson. However there is still a way to write polymorphic
functions. This is especially useful to write reusable
code. Polymorphic functions are transformed into several monomorphized
versions. For instance a function f : 'a option -> int
will be
transformed into two functions f_i : int option
and
f_Ln : nat list option
if it is used with both an integer argument
and a list of naturals argument in the code.
To make this extension even more useful, Liquidity allows user declared type to be parameterized by one or more type variables. Every type variable that appears in the type definition must also appear in the type name declaration (on the left hand side).
The following example defines a record type ('a, 'b) t
with two
fields whose type are parameters. The function mk_t
builds values
of type t
with it argument. mk_t
has the polymorphic type
mk_t : ('a * 'b) -> ('a, b') t
.
type storage = int
type ('a, 'b) t =
{ x : 'a ; y : 'b }
let[@noinline] mk_t (x, y) =
{ x; y }
let%entry default parameter _ =
let w = mk_t (parameter, 99) in
if not w.x then failwith ();
let v = mk_t (false, Some 0) in
if v.x then failwith ();
[], w.y
The type of storage cannot be polymorphic, however it can contain
weak type variables (like '_a
), which means they must be the
same for every instance (i.e. there can only be one instance of type
storage
). For example writing type '_a storage = '_a
allows
type storage to be inferred.
ReasonML Syntax¶
Liquidity supports two syntaxes:
- OCaml syntax (OCaml with Dune-specific changes)
- ReasonML syntax (with Dune-specific changes)
ReasonML Compiler Arguments¶
By default, the compiler uses expects the OCaml syntax, and outputs
files in OCaml syntax. This behavior changes with the file extension
and with the --re
argument. Files that end in .reliq
will be
parsed as ReasonML Liquidity files. The decompiler will ouptu files in
ReasonML syntax when given the flag -re
. If your file is
test.reliq
, you can compile it using:
liquidity test.reliq
You can also convert a file from one syntax to another, using the
--convert FILE
argument. For example, a file in OCaml-syntax can
be converted to ReasonML syntax:
$ liquidity --convert test19.liq
type storage = {
key,
hash: bytes,
c: address,
};
let%init storage: storage = {
key: 0x0085b1e4560f47f089d7b97aabcf46937a4c137a9c3f96f73f20c83621694e36d5,
hash: 0xabcdef,
c: KT1LLcCCB9Fr1hrkGzfdiJ9u3ZajbdckBFrF,
};
contract PlusOne = {
type storage = int;
type t =
| A
| B;
let%init init_storage = (x: bool, y: int) =>
if (x == false) {
0;
} else {
y;
};
let%entry default = (_: unit, s) => ([], s + 1);
};
let%entry default = (sign: signature, storage) => {
let x = PlusOne.A;
switch (x) {
| PlusOne.B => failwith()
| _ => ()
};
let c = Contract.self();
let key_hash = Crypto.hash_key(storage.key);
if (key_hash == tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx) {
Current.failwith();
};
if (key_hash
== Crypto.hash_key(
edpkuTXkJDGcFd5nh6VvMz8phXxU3Bi7h6hqgywNFi1vZTfQNnS1RV,
)) {
Current.failwith();
};
let delegate = Some(key_hash);
let spendable = Crypto.check(storage.key, sign, storage.hash);
let amount = Current.amount();
let amount =
switch (amount / 2p) {
| None => Current.failwith() /* not possible */
| Some(qr) => qr
};
let delegatable = false;
let _cocococ = [%handle PlusOne.default](storage.c);
let _op1 = Self.default(sign, ~amount=0DUN);
let (c_op, c_addr) =
Contract.create(
~delegate,
~amount=amount[0],
~storage=9,
(contract PlusOne),
);
let storage = storage.c = c_addr;
([c_op], storage);
};
The same file can be converted back and forth:
$ liquidity --convert test19.liq > test19.reliq
$ liquidity --convert test19.reliq > test19.liq
Beware however that the conversion from ReasonML syntax back to the OCaml one erases the comments.
ReasonML Syntax Extensions¶
Liquidity borrows most of ReasonML syntax, with a few changes, similar to the changes in the OCaml syntax:
- The
module
keyword is replaced by thecontract
keyword, to define contracts and contract signatures - Dune-specific literals are available, such as
12.2DUN
,dn1c35okrd97ZfiH6X2j8DiD3dSkCqVkGkZN
, etc. - Tezos-specific literals are available, such as
12.2tz
,tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
, etc.
A good way to learn this syntax is to use the syntax conversion
argument of the compiler (--convert FILE
).