A First Encounter with SmartPy

SmartPy.io
Jun 27, 2019 · 9 min read

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:

A few remarks:

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:

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.

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.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store