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 field x

  • 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 to self.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')
              )

With this, you complete this workshop successfully!!