Beginner
Smart contract development with SmartPy
Welcome to our tutorial on smart contract development with SmartPy, a powerful smart contract development framework for the Tezos blockchain network. In this tutorial, we will cover the basics of creating and deploying smart contracts using the SmartPy framework. We will walk through the process of writing, testing, and deploying a smart contract, as well as explaining the key features and advantages of using SmartPy.
By the end of this tutorial, you will have a solid understanding of how to write, test, and deploy smart contracts using the SmartPy framework on the Tezos network. You will also learn best practices for developing smart contracts in SmartPy and how to interact with them. This tutorial is ideal for developers and researchers interested in creating and deploying smart contracts on the Tezos blockchain, using the SmartPy framework which offers a high-level language to write contracts, a powerful testing framework and a rich IDE.
About SmartPy
SmartPy is a Python library. SmartPy scripts are regular Python scripts that use SmartPy constructions. This mechanism is useful because it brings very powerful meta-programming capabilities.
Meta-programming is when we can write a program that writes a program, i.e., constructs a contract. Indeed, the functions of the SmartPy Library are used to construct a smart contract.
Smart contracts are executed once they are deployed in the Tezos blockchain (although they can be simulated).
Like most languages, SmartPy has expressions. For example:
-
self.data.x
represents the contract storage fieldx
-
2
represents the number `2 -
self.data.x + 2
represents their sum
Inside a contract, when we write
y = self.data.x + 2
we declare y
as an alias the SmartPy expression self.data.x + 2
.
Note that the actual addition is not carried out until the contract has been deployed and the entrypoint is called.
As you will see throughout this tutorial, SmartPy is a library that will be imported in the following way:
import smartpy as sp
And the functions of SmartPy will be called with the prefix sp.
. For example:
sp.verify(self.data.x > 2)
Here, sp.verify()
checks that the field x
is larger than 2
and raises an error if it is not. This is performed at run time, i.e., in the blockchain, once translated into Michelson.
Since Python does not 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.
For example, we will use:
sp.if self.data.x > 2: self.data.x += 1
If we would have used the if
native to Python it could not be interpreted and compiled in Michelson
About the raffle contract
A raffle is a game of chance that distributes a winning prize.
The organizer is in charge of defining a jackpot and selling tickets that will either be winners or losers. In the case of our example, we will only have one winning ticket.
Three entrypoints allow interaction with the contract:
-
open_raffle can only be called by the administrator. During this call, he sends the tez amount of the jackpot to the contract, defines a closing date, indicates the number/identity of the winning ticket (in an encrypted way), and declares the raffle open.
-
buy_ticket allows anyone to buy a ticket for 1 tez and take part in the raffle.
-
close_raffle can only be called by the administrator. It closes the raffle and sends the jackpot to the winner.
Get started
This section illustrates the coding of the smart contract in the online editor proposed by SmartPy. You can however also use your favorite IDE instead, as described previously.
Create your contract
To start, create a new contract in the online editor and name it Raffle Contract.
Template
Copy/paste the template below to get started:
# Raffle Contract - Example for illustrative purposes only. import smartpy as sp class Raffle(sp.Contract): def __init__(self): self.init() @sp.entry_point def open_raffle(self): pass @sp.add_test(name = "Raffle") def test(): r = Raffle() scenario = sp.test_scenario() scenario.h1("Raffle") scenario += r
A SmartPy contract is a class definition that inherits from the sp.Contract
.
A class is a code template for creating objects. Objects have member variables and have a behaviour associated with them. In python a class is created by the keyword
class
.
Inheritance allows us to define a class that can inherit all the methods and properties of another class.
-
The SmartPy storage is defined into the constructor
__init__
which makes a call toself.init()
that initializes the fields and sets up the storage. -
Entrypoints are a method of a contract class that can be called on from the outside. Entrypoints need to be marked with the
@sp.entry_point
decorator.Decorators are functions that modify the functionality of other functions. They are introduced by
@
and are placed before the function.
Test Scenarios are good tools to make sure our smart contracts are working correctly.
-
A new test is a method marked with the
sp.add_test
decorator. -
A new scenario is instantiated by
sp.test_scenario
. -
Scenarios describe a sequence of actions: originating contracts, computing expressions or calling entry points, etc.
-
In the online editor of SmartPy.io, the scenario is computed and then displayed as an HTML document on the output panel.
We will explain in more detail the use of all these concepts in the next sections.
Our code doesn’t do much for now, but it can already be compiled by pressing the run button. If there is no error, you should be able to visualize the generated Michelson code in the Deploy Michelson Contract tab.
parameter (unit %open_raffle); storage unit; code { CDR; # @storage # == open_raffle == # @storage NIL operation; # list operation : @storage PAIR; # pair (list operation) @storage };
open_raffle entrypoint
open_raffle
is the entrypoint that only the administrator can call. If the invocation is successful, then the raffle will open, and the smart contract’s storage will be updated with the chosen amount and the hash of the winning ticket number.
Code
Here is the first version of this contract. We will go through its different parts one at a time.
# Raffle Contract - Example for illustrative purposes only. import smartpy as sp class Raffle(sp.Contract): def __init__(self, address): self.init(admin=address, close_date=sp.timestamp(0), jackpot=sp.tez(0), raffle_is_open=False, hash_winning_ticket=sp.bytes('0x') ) @sp.entry_point def open_raffle(self, jackpot_amount, close_date, hash_winning_ticket): sp.verify_equal(sp.source, self.data.admin, message="Administrator not recognized.") sp.verify(~ self.data.raffle_is_open, message="A raffle is already open.") sp.verify(sp.amount >= jackpot_amount, message="The administrator does not own enough tz.") today = sp.now in_7_day = today.add_days(7) sp.verify(close_date > in_7_day, message="The raffle must remain open for at least 7 days.") self.data.close_date = close_date self.data.jackpot = jackpot_amount self.data.hash_winning_ticket = hash_winning_ticket self.data.raffle_is_open = True @sp.add_test(name="Raffle") def test(): alice = sp.test_account("Alice") admin = sp.test_account("Administrator") r = Raffle(admin.address) scenario = sp.test_scenario() scenario.h1("Raffle") scenario += r scenario.h2("Test open_raffle entrypoint") close_date = sp.timestamp_from_utc_now().add_days(8) jackpot_amount = sp.tez(10) number_winning_ticket = sp.nat(345) bytes_winning_ticket = sp.pack(number_winning_ticket) hash_winning_ticket = sp.sha256(bytes_winning_ticket) scenario.h3("The unauthorized user Alice unsuccessfully call open_raffle") scenario += r.open_raffle(close_date=close_date, jackpot_amount=jackpot_amount, hash_winning_ticket=hash_winning_ticket) \ .run(source=alice.address, amount=sp.tez(10), now=sp.timestamp_from_utc_now(), valid=False) scenario.h3("Admin unsuccessfully call open_raffle with wrong close_date") close_date = sp.timestamp_from_utc_now().add_days(4) scenario += r.open_raffle(close_date=close_date, jackpot_amount=jackpot_amount, hash_winning_ticket=hash_winning_ticket) \ .run(source=admin.address, amount=sp.tez(10), now=sp.timestamp_from_utc_now(), valid=False) scenario.h3("Admin unsuccessfully call open_raffle by sending not enough tez to the contract") close_date = sp.timestamp_from_utc_now().add_days(8) scenario += r.open_raffle(close_date=close_date, jackpot_amount=jackpot_amount, hash_winning_ticket=hash_winning_ticket) \ .run(source=admin.address, amount=sp.tez(5), now=sp.timestamp_from_utc_now(), valid=False) scenario.h3("Admin successfully call open_raffle") scenario += r.open_raffle(close_date=close_date, jackpot_amount=jackpot_amount, hash_winning_ticket=hash_winning_ticket) \ .run(source=admin.address, amount=sp.tez(10), now=sp.timestamp_from_utc_now()) scenario.verify(r.data.close_date == close_date) scenario.verify(r.data.jackpot == jackpot_amount) scenario.verify(r.data.raffle_is_open) scenario.h3("Admin unsuccessfully call open_raffle because a raffle is already open") scenario += r.open_raffle(close_date=close_date, jackpot_amount=jackpot_amount, hash_winning_ticket=hash_winning_ticket) \ .run(source=admin.address, amount=sp.tez(10), now=sp.timestamp_from_utc_now(), valid=False)
Storage definition
def __init__(self, address): self.init(admin=address, close_date=sp.timestamp(0), jackpot=sp.tez(0), raffle_is_open=False, hash_winning_ticket=sp.bytes('0x') )