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.entry_point decorator. For example the following entry point checks that the argument given is larger than 2:

    @sp.entry_point
    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.entry_point
    def setX(newX):
        self.data.x = newX

    @sp.entry_point
    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, True, False, sp.bool(True) and sp.bool(False).

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

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

  • sp.TIntOrNat: The type of integer values whose type is still undetermined between sp.TInt or sp.TNat, e.g. 42 or sp.intOrNat(42).

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

  • sp.TBytes: The type of serialized data, e.g. sp.pack(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.TContract(t): A contract whose parameter is of type t.

  • 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.set_type(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.big_map(l = …​, tkey = …​, tvalue = …​): Defines a big_map 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.to_int.

Int vs Nat

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

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

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

  • sp.as_nat(i): Converts an int into a nat and fails if not possible. It is implemented as sp.as_nat(i) = sp.is_nat(i).open_some().

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.

String Operators

  • e1 + e2: Concatenates two strings.

  • sp.concat(l): Concatenates a list l of strings.

  • sp.len(e): Returns the length of e.

  • sp.slice(expression, offset, length): Slices expression from offset for length characters. sp.slice(expression, offset, length) is of type sp.TOption(sp.TString).

Bytes Operators

  • e1 + e2: Concatenates two bytes.

  • sp.concat(l): Concatenates a list l of bytes.

  • sp.len(e): Returns the length of e.

  • sp.slice(expression, offset, length): Slices expression from offset for length characters. sp.slice(expression, offset, length) is of type sp.TOption(sp.TBytes).

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

  • sp.unpack(x, t = …​): Parses the serialized data from its optimized binary representation. There is an optional argument t to fix the type. sp.unpack(e, t) is of type sp.TOption(t).

  • sp.bytes('0x…​'): Introduces a sp.TBytes in hexadecimal notation.

  • See Cryptography in Smart Contracts for hash functions.

Timestamp Operators

  • sp.now: The minimal injection time on the stack for the current block/priority. For all reasonable purposes, this is a technical detail and sp.now should be understood as the timestamp of the block whose validation triggered the execution.

  • sp.timestamp(…​): Returns a constant timestamp.

  • e.add_seconds(seconds): Returns a timestamp with seconds added to e, where e must be a sp.TTimestamp and seconds a sp.TInt.

  • e1 - e2: Returns the difference in seconds between two timestamps.

Mutez Operators

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

  • sp.balance: The balance of the current contract.

  • e1 + e2 and e1 - e2: Usual arithmetic operators on sp.TMutez.

  • sp.split_tokens(m, quantity, totalQuantity): Computes m * quantity / totalQuantity where m is of type sp.TMutez, and quantity and totalQuantity are of type sp.TNat.

Contract and Address Operators

  • sp.self: The current contract of type sp.TContract(t) for some type t.

  • sp.to_address(contract): Computes the address, of type sp.TAddress, of a contract of type sp.TContract(t) for some type t.

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

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

  • sp.contract(t, address, entry_point = ""):

    • When optional parameter entry_point is empty or unspecified, it returns sp.some(c), where c is a contract handle of type sp.TContract(t), if address, of type sp.TAddress, points to a contract that expects a parameter of type t. Otherwise it returns sp.none.

    • When entry_point is not empty, it returns the specific entry point specified by the string entry_point of the contract. t must match the entry point’s expected parameter type. Otherwise, it returns sp.none.

  • sp.transfer(arg, amount, destination): Call the destination contract with argument arg while sending the specified amount to it. Note that destination must be of type sp.TContract(t). The type of arg must be t, i.e. the argument sent to the destination must be consistent with what it expects.

  • sp.send(destination, amount): Send the specified amount to the destination contract. Will fail if destination (of type sp.TAddress) does not resolve to a contract that expects a sp.TUnit argument (e.g. an account that does not result in any actions).

  • sp.implicit_account: Please see Key Hash Operators

Example: Suppose we have an address a of a contract with an entry point "foo" that expects an integer. To call it, we first obtain a handle to the entry point:

  c = sp.contract(sp.TInt, a, entry_point = "foo").open_some()

The call to open_some() asserts that the address resolved successfully and that the referenced entry point indee expects an integer. Now that we have our handle c, we can call the contract e.g. with the argument -42 while sending along 0 tokens:

  sp.transfer(-42, sp.mutez(0), c)

Key Hash Operators

  • sp.set_delegate(baker): Sets or unsets an optional baker of type sp.TOption(sp.TKeyHash).

  • sp.implicit_account(key_hash): Returns the implicit account of type sp.TContract(sp.TUnit) from a sp.TKeyHash.

Building containers

  • Convention. Container constructor names are uncapitalized and their types are capitalized. sp.map(…​) of type sp.TMap(…​), sp.big_map(…​) 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.big_map(l = …​, tkey = …​, tvalue = …​): Defines a big_map 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.

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

Container access

The following expressions are used to access containers:

  • e[key]: looks up an entry in a map or a big_map. Fails if the entry is not found. 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 (not a big_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 (not a big map).

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

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

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

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

  • e.open_some(): 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.is_some(): For an optional value, check whether it is sp.some(…​).

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

e.is_variant(v): For a variant, checks whether it is sp.variant(v, …​).

e.open_variant(v): If e is equal to sp.variant(v, x), return x. Otherwise fail.

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.local("x", 0)

The first argument to sp.local 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 variable values can be accessed to and updated with the .value field: x.value = 1, x.value = 2 * x.value + 5, etc.

This is mostly useful in loops.

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

As an example, here is how we can commute a square root.

    @sp.entry_point
    def squareRoot(self, x):
        sp.verify(x >= 0)
        y = sp.local('y', x)
        sp.while y.value * y.value > x:
            y.value = (x // y.value + y.value) // 2
        sp.verify((y.value * y.value <= x) & (x < (y.value + 1) * (y.value + 1)))
        self.data.value = y.value

Modifying Containers

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

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

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

  • 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.if_ 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).

Raising Exceptions

sp.failwith(message) aborts the current transaction and raises a message of arbitrary type. This cannot be caught.

Cryptography

Cryptography in Smart Contracts

The following cryptographic functions are available:

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

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

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

Cryptography in Test Scenarios

Also, only within test scenarios, one can use:

  • The class alice = sp.test_account("Alice"): Create a deterministic key-pair from a “seed” string.

    • alice.address: Get the public-key-hash as a TAddress.

    • alice.public_key_hash: Get the public-key-hash as a TKeyHash.

    • alice.public_key: Get the full public-key as a TKey.

    • alice.secret_key: Get the secret-key as a TString.

  • sp.make_signature(secret_key, message, message_format = 'Raw'): Forge a signature compatible with sp.check_signature(…​); the message is a TBytes value (usually the result of an sp.pack call), the message_format can also be "Hex" in which case the message will be interpreted as an hexadecimal string.

sp.test_account methods and sp.make_signature are not available for compilation to Michelson (a smart contract cannot manipulate secret keys).

Tests and Test Scenarios

This has been introduced by the following Medium Post.

Tests

Adding a Test

Tests are added by doing:

  @sp.add_test(name = "First test")
  def test():
    ...

Defining a Scenario

Scenarios are defined in a test, by doing:

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

Test Example

  @sp.add_test(name = "First test")
  def test():
      # We define a test scenario, called scenario,
      # together with some outputs and checks
      scenario = sp.test_scenario()
      # 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)

Test Accounts

Test accounts can be defined by calling sp.test_account(seed) where seed is a string. A test account account contains some fields: account.address, account.public_key_hash, account.public_key, and account.secret_key.

  admin = sp.test_account("Administrator")
  alice = sp.test_account("Alice")
  bob   = sp.test_account("Robert")

They can be used for several purposes: getting addresses with account.address, in sender or source parameters or for checking or creating signatures.

Registering and Displaying Calls to Entry Points

  scenario += c1.myEntryPoint(12)
  scenario += c1.myEntryPoint(...).run(sender = ..., source = ..., 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. It can be either built by a sp.test_account(…​) or sp.address(…​).

  • source: the simulated source of the transaction. It populates sp.source. It can be either built by a sp.test_account(…​) or sp.address(…​).

  • 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). It populates sp.now.

  • 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")

Showing Expressions

To compute expressions, we use scenario.show(expression, html = True, stripStrings = False).

  scenario.show(expression, html = True, stripStrings = False)
  # html: True by default, False to export not in html but like in source code.
  # stripStrings: False by default, True to remove quotes around strings.

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

Computing Expressions

To compute expressions, we use scenario.compute.

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

The variable x can now be used in the sequel of the scenario and its value is fixed.

Checking Assertions

To verify conditions, we use scenario.verify. To verify an equality condition, we can also use scenario.verify_equal which works on both comparable and non-comparable types.

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

  scenario.verify_equal(c1.data.myList == [2, 3, 5, 7])

Interactive Testing

To test interactively a contract, we use scenario.simulation. It also provides a step-by-step mode that is very usefull to understand some computation.

  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