SmartPy is a Python library for constructing Tezos smart contracts. It comes with a compiler that generates Michelson code.

Building Blocks

Expressions

Like most languages, SmartPy has expressions. For example self.data.x represents the contract storage field x and 2 represents the number 2, whereas self.data.x + 2 represents their sum.

Commands

Commands do something. For example, sp.verify(self.data.x > 2) checks that the field x is larger than 2 and raises an error if it isn’t.

Entry Points

An entry point is a method of a contract class that can be called from the outside. They need to be marked with the @sp.entryPoint decorator. For example the following entry point checks that the argument given is larger than 2:

    @sp.entryPoint
    def checkLargerThanTwo(p):
        sp.verify(p > 2)

Contracts

A SmartPy contract consists of a state together with one or several entry points. It is a class definition that inherits from sp.Contract. The constructor (__init__) makes a call to self.init and initializes fields that make up the contract’s state.

class Hello(sp.Contract):
    def __init__(self):
        self.init(x = 0)

    @sp.entryPoint
    def setX(newX):
        self.data.x = newX

    @sp.entryPoint
    def checkLargerThanX(p):
        sp.verify(p > self.data.x)

Types

Types are usually automatically infered and not explicitely needed. SmartPy types are all of the form sp.T<TypeName>.

Meta-Programming

The functions described here are used to construct a smart contract. Smart contracts are executed once they are deployed in the Tezos block chain (although they can be simulated). This is indeed meta-programming: we can write a programm that writes a program (a.k.a. constructs a contract).

Note that in the example self.data.x + 2, the actual addition isn’t carried out until the contract has been deployed and the entry point is called.

Types

Type Inference

Just like in Python, most of the time there is no need to specify the type of an object in SmartPy. For a number of reasons (e.g. because SmartPy’s target language, Michelson, requires types), each SmartPy expression does however need a type. Therefore SmartPy uses type inference in order to determine each expressions type.

In practice this means that information about an expression is gathered according to its usage: for example, when somewhere in your contract you write self.data.x == "abc", SmartPy will automatically determine and remember that self.data.x is a string.

Note that SmartPy types are distinct from Python types: self.data.x == "abc" has the Python type sp.Expr (simply because it is a SmartPy expression), whereas it has the SmartPy type TBool (see below).

While most of the time the user will not write many types explicitly it is beneficial to at least have a basic understanding of what they are. This also helps understanding error messages better.

Primitive Data Types

SmartPy has the following primitive types:

  • sp.TUnit: A type with a single value, namely sp.unit.

  • sp.TBool: The type of boolean values, sp.bool(True) and sp.bool(False).

  • sp.TInt: The type of integer values, e.g. sp.int(-42).

  • sp.TNat: The type of non-negative integer values, e.g. sp.nat(42).

  • sp.TString: The type of strings, e.g. sp.string("abc").

  • sp.TBytes: The type of serialized data, e.g. sp.pack(sp.int(42))

Container Types

SmartPy has a few built-in data structures. Their types are:

  • sp.TPair: The type of pairs, e.g. (1, True).

  • sp.TList: The type of lists, e.g. [1, 2, 3].

  • sp.TSet: The type of sets, e.g. {1, 2, 3}.

  • sp.TMap: The type of maps, e.g. {'A': 65, 'B': 66, 'C'; 67}.

  • sp.TBigMap: The type of big maps, e.g. {'A': 65, 'B': 66, 'C'; 67}.

  • sp.TOption: The type of optional values.

  • There is no array in SmartPy because they are missing in Michelson.

Tezos-specific data types

A few data types are important in the context of smart contracts:

  • sp.TMutez: The type of Tezos tokens, e.g. sp.mutez(42000) stands for 0.042 Tez, wheras sp.tez(42) stands for 42 Tez.

  • sp.TTimestamp: A moment in time, e.g. sp.timestamp(1571761674). The argument to sp.timestamp is in "epoch" format, i.e. seconds since 1970-01-01.

  • sp.TAddress: An address of a contract or account, e.g. sp.address("tz1YtuZ4vhzzn7ssCt93Put8U9UJDdvCXci4").

  • sp.TKey: A public cryptographic key.

  • sp.THash: The hash of a public cryptographic key.

  • sp.TSignature: A cryptographic signature.

Derived Data Types

Derived data types can be used in several ways:

  • If t is a data type, then sp.TOption(t) is also a data type. Its value is either sp.none or sp.some(x), where x is any value of type t.

Optional values are useful for accomodating missing data: e.g. if your contract has an optional expiry date, you can add a field expiryDate = sp.none to the constructor. Then, if you want to set the expiry date, you write expiryDate = sp.some(sp.timestamp(1571761674)). Conversely, if you want to unset it again, you write expiryDate = sp.none. SmartPy automatically infers the type sp.TOption(sp.TTimestamp) for x, so you don’t have to make it explicit.

  • A Record type is defined simply by enumerating the field names together with types, e.g. sp.TRecord(x = sp.TInt, y = sp.TInt).

A value of this type is written sp.record(x = 2, y = 3).

Setting a type constraint in SmartPy

This is usually not needed.

  • sp.setType(e, t): Constrains e to be of type t. This can be used as an expression or a command.

Containers have built-in optional constraint arguments.

  • sp.map(l = …​, tkey = …​, tvalue = …​): Defines a map of (optional) elements in l with optional key type tkey and optional value type tvalue.

  • sp.bigMap(l = …​, tkey = …​, tvalue = …​): Defines a bigMap of (optional) elements in l with optional key type tkey and optional value type tvalue.

  • sp.set(l = …​, t = …​): Defines a set of (optional) elements in l with optional element type t.

  • sp.list(l = …​, t = …​): Defines a list of (optional) elements in l with optional element type t.

SmartPy Expressions

SmartPy expressions have the Python type sp.Expr. In this class many methods are overloaded so to provide convient syntax: e.g. we can write self.data.x + 2 for the SmartPy expression that represents the sum of the storage field x and 2.

SmartPy expressions have type sp.Expr. The on-chain evaluation of an expression does not have any side effects.

Any Python literal (string or integer) that is used in place of an sp.Expr is automatically converted. Thus we can write self.data.x + 1 instead of self.data.x + sp.int(1).

Arithmetic Operators

The arithmetic operators +, -, *, %, // behave just like in Python.

In SmartPy, type inference of arithmetic operators imposes that both sides have the same type. This constraint can be relaxed by explicitly using sp.toInt.

Int vs Nat

  • abs(i): Returns the absolute value of i. abs converts an int into a nat.

  • sp.toInt(n): Converts a nat into an int.

  • sp.isNat(i): Converts a int into an nat option. sp.isNat(i) = sp.some(n) when i is an integer and sp.none otherwise.

  • sp.asNat(i): Converts an int into a nat and fails if not possible. It is implemented as sp.asNat(i) = sp.isNat(i).openSome().

Division

  • The operator / performs truncated integer division when applied to SmartPy expression, just like // does. This is different to Python 3 (where / doesn’t truncate and yields a float when applied to integers).

  • sp.ediv(num, den): Performs Euclidian division.

Comparison Operators

The comparison operators ==, !=, <, <=, >, >= behave just like in python. They return a boolean.

sp.min(x, y) and sp.max(x, y) return the minimum and maximum of x and y, respectively.

Logical Operators

SmartPy has the following logical operators:

  • ~ e: Returns the negation of e, where e must be a boolean.

  • e1 | e2: Returns True if e1 is True, otherwise e2. Both e1 and e2 must be booleans.

  • e1 & e2: Returns False if e1 is False, otherwise e2. Both e1 and e2 must be booleans.

Note that, unlike in Python, & and | do short-circuiting on SmartPy expressions: for example, the evaluation of (x==x) | (self.data.xs[2] == 0)) will not fail. Also, not, and, and or cannot be used to construct SmartPy expressions. Use ~, &, and | instead.

Building containers

  • Convention. Container constructor names are uncapitalized and their types are capitalized. sp.map(…​) of type sp.TMap(…​), sp.bigMap(…​) of type sp.TBigMap(…​), sp.set(…​) of type sp.TSet(…​), sp.list(…​) or type sp.TList, sp.pair(…​) of type sp.TPair(…​), etc.

  • sp.list(l = …​, t = …​): Defines a list of (optional) elements in l whose optional type is t.

  • sp.map(l = …​, tkey = …​, tvalue = …​): Defines a map of (optional) elements in l with optional key type tkey and optional value type tvalue.

  • sp.bigMap(l = …​, tkey = …​, tvalue = …​): Defines a bigMap of (optional) elements in l with optional key type tkey and optional value type tvalue.

  • sp.set(l = …​, t = …​): Defines a set of (optional) elements in l with optional element type t.

  • sp.pair(e1, e2): Defines a pair of two elements.

  • sp.some(e): Defines an optional value containing an element e.

  • sp.none: Defines an optional value not containing any element.

  • Lists, pairs and maps can also be defined with a regular Python value such as [1, 2, 3], (1, True), {"aaa": 1, "bbb": 2}.

  • Sets can also be defined using {1, 2, 3}. This only works with non-SmartPy specific expressions. For SmartPy expressions, we must use sp.set([e1, e2, …​, en]).

  • There is no array in SmartPy because they are missing in Michelson, we use maps instead. There are three helper functions: sp.vector(..), sp.matrix(..) and sp.cube(..) that take respectively a list, a list of lists and a list of lists of lists and return maps.

Container access

The following expressions are used to access containers:

  • e[key]: looks up an entry in a container. Fails if the entry is not found. If e is a map, key must have the type of its keys.

  • e.get(key, defaultValue = None): Same as e[key]. If defaultValue is specified and there is no entry for key in e, return defaultValue instead of failing.

  • e.elements(): Returns the sorted list of elements in a set.

  • e.items(): Returns the sorted list of key-value entries in a map. Each entry is rendered as record with the two fields key and value.

  • e.keys(): Returns the sorted list of keys of a map.

  • e.values(): Returns the list of values of a map, sorted by keys.

  • e.contains(value): Checks whether e contains the key value. This applies to sets, maps and bigMaps.

  • sp.fst(..) and sp.snd(..) to access elements in pairs.

  • e.isSome(): Checks that an optional value contains an element.

  • e.openSome(): Accesses the value contained in an optional value if it exists and fails otherwise.

Note that there is no way to perform random access on a list.

Variants

e.isSome(): For an optional value, check whether it is sp.some(…​).

e.openSome(): If e is equal to sp.some(x), return x. Otherwise fail.

Miscellaneous

e.addSeconds(seconds): Return a timestamp with seconds added to e, where e must be a timestamp.

e.append(other) (command): Shorthand for e.set(sp.append(e, other)).

Record fields

If x is a record (see below) and a one of its fields, we can obtain the field’s value by writing x.a.

Commands

Assignment

  • lhs = rhs (command): Evaluates rhs is evaluated and assigns it to lhs. Both lhs and rhs must be SmartPy expressions. Doesn’t work if lhs is a Python variable.

  • lhs.set(rhs) (command): Alternative syntax for assignment. Useful when the left-hand-side is a single Python variable, e.g. one referencing a SmartPy local variable (see below).

Local variables

Local SmartPy variables can be defined as follows: x = sp.newLocal("x", 0)

The first argument to sp.newLocal is a string that will be used in error messages. It is advisable to use the same name that is used on the left of =.

Local variables can be updated just like a contract state variable:

x.set(x + 1)

This is mostly useful in loops.

Note that local SmartPy variables are different to Python variables. The latter cannot be updated during contract execution.

Modifying Containers

  • myList.push(element): Push an element on top of a list.

  • del myMap[key]: Delete an element from a map or bigMap.

  • myMap[key] = value: Setting or replacing a element in a map or bigMap.

  • mySet.add(element): Add an element to a set.

  • mySet.remove(element): Remove an element from a set.

Control and Syntactic Sugar

Since Python doesn’t allow its control statements to be overloaded, certain language constructs are desugared by a pre-processor: sp.if, sp.else, sp.for, sp.while are SmartPy commands. (The desugared version has sp.ifBlock etc. instead.)

sp.while: A while-loop. Example:

        sp.while 1 < y:
            self.data.value += 1
            y.set(y // 2)

sp.for / sp.if / sp.else

If we use e.g. sp.if instead of a plain if, the result will be a SmartPy conditional instead of a Python one. SmartPy conditionals are executed once the contract has been constructed and has been deployed or is being simulated. On the other hand, Python conditionals are executed immediately. Therefore the condition ofter the if cannot depend on the state of the contract. When in doubt, always use the sp. prefix inside a smart contract.

Checking a Condition

sp.verify(self.data.x > 2) checks that the field x is larger than 2 and raises an error if it isn’t. This is useful to prevent an entry point from proceding if certain conditions are not met (e.g. in a contract that manages accounts a client cannot withdraw more money than they deposited).

Interacting With The Blockchain

  • sp.amount: The amount of the current transaction.

  • sp.sender: The contract that called the current entry point.

  • sp.source: The contract that initiated the current transaction. May or may not be equaul to sp.sender.

  • sp.send(dest, amount): Send the specified amount to the dest contract/account.

Cryptography

The following cryptographic functions are available:

  • sp.hashKey(key): Compute the b58check of key (which must be of type TKey). Returns a TKeyHash value.

  • sp.checkSignature(k, s, b): Determine whether the signature s (a TSignature value) has been produced by signing b (a TBytes value) with the private key corresponding to k (a TKey public key value).

  • The functions sp.blake2b, sp.sha512, sp.sha256 take a TBytes value and return the corresponding hash as a new TBytes value.

Miscellaneous

  • sp.now: The timestamp of the block whose validation triggered the execution.

  • sp.range(x, y): A list from x (inclusive) to y (exclusive). Useful in conjunction with sp.for loops.

  • sp.pack: Serializes a piece of data to its optimized binary representation. Returns an object of type TBytes.

  • sp.splitTokens(m, quantity, totalQuantity): Computes m * quantity / totalQuantity.

Tests and Test Scenarios

This has been introduced by the following Medium Post.

Tests

Adding a Test

Tests are added by doing:

  @addTest(name = "First test")
  def test():
    ...

Defining a Scenario

Scenarios are defined in a test, by doing:

  @addTest(name = "First test")
  def test():
      # We define a test scenario, called scenario,
      # together with some outputs and checks
      scenario = sp.testScenario()

Test Example

  @addTest(name = "First test")
  def test():
      # We define a test scenario, called scenario,
      # together with some outputs and checks
      scenario = sp.testScenario()
      # We first define a contract and add it to the scenario
      c1 = MyContract(12, 123)
      scenario += c1
      # And send messages to some entry points of c1
      scenario += c1.myEntryPoint(12)
      scenario += c1.myEntryPoint(13)
      scenario += c1.myEntryPoint(14)
      scenario += c1.myEntryPoint(50)
      scenario += c1.myEntryPoint(50)
      scenario += c1.myEntryPoint(50).run(valid = False) # this is expected to fail
      # Finally, we check the final storage of c1
      scenario.verify(c1.data.myParameter1 == 151)

In a Test Scenario

Registering and displaying contracts

  scenario += c1
  # This is identical to doing
  scenario.register(c1, show = True)
  # To only register the smart contract but not show it
  scenario.register(c1)

Registering and Displaying Calls to Entry Points

  scenario += c1.myEntryPoint(12)
  scenario += c1.myEntryPoint(...).run(sender = ..., amount = ..., now = ..., valid = ...)
  # To only execute a call to an entry point but not show it
  scenario.register(c1.myEntryPoint(12))

The run method and its parameters are all optional.

  • sender: the simulated sender of the transaction. It populates sp.sender.

  • amount: the amount sent. Example: amount = sp.tez(10) or amount = sp.mutez(10000). It populates sp.amount.

  • now: the timestamp of the transaction. Example: sp.timestamp(1571761674).

  • valid: the expected validity of the transaction. True by default. If the validity of a transaction doesn’t match its expected validity, SmartPy shows an alert.

Adding Document Informations

  scenario.h1("a title")
  scenario.h2("a subtitle")
  scenario.h3(..)
  scenario.h4(..)
  scenario.p("Some text")

Computing Expressions

To compute expressions, we use scenario.compute.

  scenario.compute(c1.data.myParameter1 * 12)
  scenario.compute(c1.data)

Checking Assertions

To verify conditions, we use scenario.verify.

  scenario.verify(c1.data.myParameter == 51)

Interactive Testing

To test interactively a contract, we use scenario.simulation. Note that this is very experimental.

  scenario.simulation(c1)

Command Line Interface

The command line interface is called SmartPyBasic and has been introduced by the following Medium Post.

Installation

Installation in the ~ directory can be done by typing

# for the official stable version
sh <(curl -s https://SmartPy.io/SmartPyBasic/SmartPy.sh) local-install ~
# for the dev version (not as stable as the stable version but reasonably stable, new incremental features are here)
sh <(curl -s https://SmartPy.io/SmartPyBasicDev/SmartPy.sh) local-install-dev ~
# for the test version (quite experimental)
sh <(curl -s https://SmartPy.io/SmartPyBasicTest/SmartPy.sh) local-install-test ~

Dependencies

SmartPyBasic depends on python3 and node.js.

Execution

Executing a SmartPy Script

~/SmartPyBasic/SmartPy.sh run <myscript.py>

This runs an arbitrary Python script that can happen to contain SmartPy code.

Executing a SmartPy Script with its tests

~/SmartPyBasic/SmartPy.sh test <myscript.py> <output-directory>

This includes many outputs: types, generated michelson code, pretty-printed scenario, etc.

Compiling a SmartPy Script

~/SmartPyBasic/SmartPy.sh compile <contractBuilder.py> <class-call> <output-directory>

Example:

~/SmartPyBasic/SmartPy.sh compile welcome.py "Welcome(12,123)" /tmp/welcome