A First Encounter with SmartPy

SmartPy.io
9 min readJun 27, 2019

SmartPy, a smart contract Python library for Tezos, has been under heavy development for the past few months. Its online editor is now available in the form of a first public alpha-version at https://SmartPy.io/demo.

It’s not finished yet but we think it is already useful and interesting for members of the community to have a first look.

One primary idea behind SmartPy is that its design should aim for simplicity and efficiency whenever possible. Its syntax is quite natural and knowing Python should be enough to understand how a smart contract will behave. Let’s start with a few examples.

A first regular Python script in SmartPy.io

SmartPy is a Python library and SmartPy.io lets its users execute Python scripts in a browser. Let’s start with a simple computation.

def f(x):
return 2 * x - 3
alert(sum(f(x) for x in range(0, 12)))

This has nothing to do with a smart contract or Tezos. It’s simply a script that performs some random computation and shows its result to the user by calling the alert function.

To test this code, you can copy-paste it into https://SmartPy.io/demo or click here.

Once in SmartPy.io, this code can be executed by clicking on the Exec icon or by using the shortcut Ctrl-Enter or Cmd-Enter while focus is in the editor.

This possibility of computing a random Python script is important as it enables meta-programming: developers can interleave regular Python computations and smart contract programming, seamlessly yielding a very powerful programming experience.

SmartPy.io is capable of executing reasonable Python3 scripts thanks to the great Brython interpreter. Reasonable means that not all Python is covered but a huge portion of it is.

A first example: building a TicTacToe game

To build a smart contract, we define a regular Python class TicTacToe that inherits from a Contract class of the smartpy module.

import smartpy as spclass TicTacToe(sp.Contract):
def __init__(self):
self.init(nbMoves = 0,
winner = 0,
draw = False,
deck = [[0, 0, 0], [0, 0, 0], [0, 0, 0]])
@sp.entryPoint
def play(self, params):
# contract code is coming here
pass

This contract calls an initialization method self.init, contained in sp.Contract, that is able to determine an initial storage for the contract, iterate on its entry points, and actually build the smart contract. Here, we’re building a TicTacToe game so we need to keep track of the deck, a potential winner or draw, and the number of moves (to possibly declare a draw).

You can copy-paste the code into SmartPy.io or click here to execute it.

Nothing observable happens until now: we only defined a class.

Using a test

We need to define an object, i.e., the smart contract instance, and interact with it. This can be done directly in SmartPy.io, but it is usually done by adding a test: a clean way to split the definition of a class and some interactions to study a given instance.

import smartpy as sp
class TicTacToe(sp.Contract):
def __init__(self):
self.init(nbMoves = 0,
winner = 0,
draw = False,
deck = [[0, 0, 0], [0, 0, 0], [0, 0, 0]])
@sp.entryPoint
def play(self, params):
# contract code is coming here
pass
if "templates" not in __name__:
@addTest(name = "Test TicTacToe")
def test():
html = ""
# define a contract
c1 = TicTacToe()
# show its representation
html += c1.fullHtml()
setOutput(html)

You can copy-paste the code into SmartPy.io or click here to execute it.

Executing this script, done by clicking on Exec or with the shortcuts Ctrl-Enter or Cmd-Enter, adds a new test Test TicTacToe. Clicking on this new link, in turn, shows some real SmartPy computation.

A shortcut Shift-Ctrl-Enter or Shift-Cmd-Enter does both steps: execution of the script and then execution of tests.

On the output panel of SmartPy.io, we now can see a few tabs:

  • Stripped: shows the state of the smart contract and its entry points, here play which happens to do nothing.
  • SmartPy: like “Stripped” but with a bit more information.
  • Data Only: data only, no entry points.
  • Types: type information about storage and entry points.
  • All: gathers information from other tabs.
  • Michelson: a Michelson script derived from the SmartPy smart contract.
  • X: simply to hide the tabs.

A few remarks:

  • The html output is generic, nothing was done for this exact example and it is nonetheless quite readable.
  • Types have been inferred from both script and storage. In Michelson, integers have two different types: int and nat. Here, SmartPy doesn’t know how the user wants to use them so infers intOrNat. Depending on some constructions, it infers int, nat or intOrNat. Similar inference is done for lists, maps, big maps, records, sum types, i.e., or-patterns, etc.
  • The Michelson compiler uses int for both int and intOrNat in SmartPy as this is the most general type and one can always require nat by explicitly using sp.nat(..) in SmartPy.

Going further with play

It is time to have this contract do something meaningful and come back to the TicTacToe class.

Let’s edit the play entry point:

    @sp.entryPoint
def play(self, params):
self.data.deck[params.i][params.j] = params.move
self.data.nbMoves += 1

and the test to obtain:

import smartpy as spclass TicTacToe(sp.Contract):
def __init__(self):
self.init(nbMoves = 0,
winner = 0,
draw = False,
deck = [[0, 0, 0], [0, 0, 0], [0, 0, 0]])
@sp.entryPoint
def play(self, params):
self.data.deck[params.i][params.j] = params.move
self.data.nbMoves += 1
# Tests
if "templates" not in __name__:
@addTest(name = "Test TicTacToe")
def test():
html = ""
# define a contract
c1 = TicTacToe()
# show its representation
html += c1.fullHtml()
html += h2("Message execution")
html += h3("A first move in the center")
html += c1.play(i = 1, j = 1, move = 1).html()
setOutput(html)

You can copy-paste the code into SmartPy.io or click here to execute it.

A few remarks:

  • Types are inferred for params as well.
  • Some Michelson code is also generated.
  • The contract pretty-printing looks really similar to the Python script.
  • A new box appears as the result of c1.play(i = 1, j = 1, move = 1).html(). It describes the environment used to evaluate play, its arguments and outputs.

Checks

What would happen if we were to add another move to the test c1.play(i = 1, j = 1, move = 2).html() ? It would be accepted since we didn’t encode checks.

To test this, you can add the following line to the script or click here.

        html   += c1.play(i = 1, j = 1, move = 2).html()

This is not correct: both players played on the same cell, this is forbidden by the official rules of the game that we all know.

This is corrected by adding a check:

        sp.verify(self.data.deck[params.i][params.j] == 0)

in

    @sp.entryPoint
def play(self, params):
sp.verify(self.data.deck[params.i][params.j] == 0)
self.data.deck[params.i][params.j] = params.move
self.data.nbMoves += 1

sp.verify(condition) verifies condition and raises an exception if false.

To test the script with this sp.verify, you can add the new line to the script or click here.

The output box for the second and bad move is now in red showing the error

WrongCondition:self.data.deck[params.i][params.j] == 0 in line: 12

Many other checks need to be performed: that players play inside the deck, that no winner has been found yet, no draw, etc.

    @sp.entryPoint
def play(self, params):
sp.verify((self.data.winner == 0) & ~self.data.draw)
sp.verify((params.i >= 0) & (params.i < 3))
sp.verify((params.j >= 0) & (params.j < 3))
sp.verify((params.move == 1) | (params.move == 2))
sp.verify(self.data.deck[params.i][params.j] == 0)
self.data.deck[params.i][params.j] = params.move
self.data.nbMoves += 1

You can add the lines to the script or click here.

Computing Winner or Draw

We can expand the simulation so that we reach a winning state.

if "templates" not in __name__:
@addTest(name = "Test TicTacToe")
def test():
html = ""
# define a contract
c1 = TicTacToe()
# show its representation
html += h2("A sequence of interactions with a winner")
html += c1.fullHtml()
html += h2("Message execution")
html += h3("A first move in the center")
html += c1.play(i = 1, j = 1, move = 1).html()
html += h3("A forbidden move")
html += c1.play(i = 1, j = 1, move = 2).html()
html += h3("A second move")
html += c1.play(i = 1, j = 2, move = 2).html()
html += h3("Other moves")
html += c1.play(i = 2, j = 1, move = 1).html()
html += c1.play(i = 2, j = 2, move = 2).html()
# assert int(c1.data.winner) == 0
html += c1.play(i = 0, j = 1, move = 1).html()
# assert int(c1.data.winner) == 1
setOutput(html)

You can add the lines to the script or click here.

Two commented-out asserts are shown in the test: the first one is OK and can be uncommented; the second one is not: we need to add code for this in our TicTacToe class.

If you uncomment the second assert, and run the test, an exception is shown and no output is shown in the output panel. This last point can be fixed by also calling setOutput(html) before calling the failing assert.

We add new lines to determine winner and draw.

    @sp.entryPoint
def play(self, params):
sp.verify((self.data.winner == 0) & ~self.data.draw)
sp.verify((params.i >= 0) & (params.i < 3))
sp.verify((params.j >= 0) & (params.j < 3))
sp.verify((params.move == 1) | (params.move == 2))
sp.verify(self.data.deck[params.i][params.j] == 0)
self.data.deck[params.i][params.j] = params.move
self.data.nbMoves += 1
r = range(0, 3)
self.checkLine([self.data.deck[params.i][j] for j in r])
self.checkLine([self.data.deck[i][params.j] for i in r])
self.checkLine([self.data.deck[i][i ] for i in r])
self.checkLine([self.data.deck[i][2 - i ] for i in r])
sp.if (self.data.nbMoves == 9) & (self.data.winner == 0):
self.data.draw = True
def checkLine(self, lin):
sp.if ((lin[0]!=0) & (lin[0]==lin[1]) & (lin[0]==lin[2])):
self.data.winner = lin[0]

You can add the lines to the script or click here.

A few words about these self.checkLine.

  • checkLine is a helper method of the contract that serves to check if three aligned cells contain identical and non-zero values.
  • It is called on both line and column of the move currently being played and on both diagonals.
  • Meta-programming is nicely at play here, checkLine is called at creation time of the smart contract, generating the corresponding code. The SmartPy code output in the “Stripped” or “SmartPy” tabs is no longer quasi identical to what was inputted; it is expanded.

Wrapping everything up

We’ve seen how to define a smart contract class, how to interact with it, and how storage, types, entry points, and generic outputs are built.

import smartpy as spclass TicTacToe(sp.Contract):
def __init__(self):
self.init(nbMoves = 0,
winner = 0,
draw = False,
deck = [[0, 0, 0], [0, 0, 0], [0, 0, 0]])
@sp.entryPoint
def play(self, params):
sp.verify((self.data.winner == 0) & ~self.data.draw)
sp.verify((params.i >= 0) & (params.i < 3))
sp.verify((params.j >= 0) & (params.j < 3))
sp.verify((params.move == 1) | (params.move == 2))
sp.verify(self.data.deck[params.i][params.j] == 0)
self.data.deck[params.i][params.j] = params.move
self.data.nbMoves += 1
r = range(0, 3)
self.checkLine([self.data.deck[params.i][j] for j in r])
self.checkLine([self.data.deck[i][params.j] for i in r])
self.checkLine([self.data.deck[i][i ] for i in r])
self.checkLine([self.data.deck[i][2 - i ] for i in r])
sp.if (self.data.nbMoves == 9) & (self.data.winner == 0):
self.data.draw = True
def checkLine(self, lin):
sp.if ((lin[0]!=0) & (lin[0]==lin[1]) & (lin[0]==lin[2])):
self.data.winner = lin[0]
# Tests
if "templates" not in __name__:
@addTest(name = "Test TicTacToe")
def test():
html = ""
# define a contract
c1 = TicTacToe()
# show its representation
html += h2("A sequence of interactions with a winner")
html += c1.fullHtml()
html += h2("Message execution")
html += h3("A first move in the center")
html += c1.play(i = 1, j = 1, move = 1).html()
html += h3("A forbidden move")
html += c1.play(i = 1, j = 1, move = 2).html()
html += h3("A second move")
html += c1.play(i = 1, j = 2, move = 2).html()
html += h3("Other moves")
html += c1.play(i = 2, j = 1, move = 1).html()
html += c1.play(i = 2, j = 2, move = 2).html()
assert int(c1.data.winner) == 0
html += c1.play(i = 0, j = 1, move = 1).html()
assert int(c1.data.winner) == 1
html += p("Player %i has won" % int(c1.data.winner))
setOutput(html)

To go further

To go further, and conquer proper SmartPy bragging rights, one can think to improve this design in several directions.

  • Add a new element to the storage called nextPlayer declaring a next player and checking, in play, that this next player plays.
  • Further expanding the design to verify that the sender of the transaction is the correct one by something similar to sp.verify(sp.sender.identity == self.data.nextPlayerAddress). This will be explained in a following tutorial.
  • Players could put some XTZ bound before playing and the winner gets everything. This will be explained and expanded with State Channels in a following post.
  • We could also play another game. Chess and Nim have templates in SmartPy.io. They are not finished in two directions: SmartPy scripts that need to be completed and Michelson compilation that still lacks some constructions. Completing the SmartPy scripts is a wonderful exercice for developers wishing to understand better how it works and get ready for more challenging SmartPy programming. For a more complete Michelson compilation, this is being addressed and will be available soon. Even when incomplete, SmartPy’s Michelson compiler tries its best to show some skeleton of a compiled script and this is already useful.

--

--

SmartPy.io

An intuitive and effective smart contracts language and development platform for Tezos. In Python.