B9lab Logo
Tezos Developer Portal
Developer PortalDeveloper Portal

SmartPy Library - Diving Into the Language

Dive deeper into the SmartPy – Typing and conditions


Reading Time: 54 min

Now, let us write a different smart contract. It shall sum all natural numbers from 0 to a given number:

import smartpy as sp

# Define Smartcontract

class Summarize(sp.Contract):
    def __init__(self):
        # Define storage with initial integer 0
        self.init(storage=0)

    # Define Entrypoint
    @sp.entry_point
    def sum(self, params):
        sp.for i in sp.range(1, params+1):
            self.data.storage+= i

@sp.add_test(name = "Second_test")
def test():
    scenario = sp.test_scenario()
    second_contract= Summarize()

    scenario+=second_contract
    scenario+= second_contract.sum(5)

So, we now have:

    @sp.entry_point
    def sum(self, params):
        sp.for i in sp.range(1, params+1):
            self.data.storage+= i

You see that SmartPy has sp.for to iterate a list, and sp.range to produce such a list of integers, like range does in Python.

Why don't we just write the following?

    @sp.entry_point
    def sum(self, params):
        for i in range(1, params+1):
            self.data.storage+= i

Because Python does not know what the parameter in the transaction or the storage on the blockchain is.

As mentioned, we can use Python for metaprogramming. So, we could write something like:

import smartpy as sp

# Define Smartcontract

class Summarize(sp.Contract):
    def __init__(self):
        # Define storage with initial integer 0
        self.init(storage=0)

    # Define Entrypoint
    @sp.entry_point
    def sum(self):
        for i in range(1, 6):
            self.data.storage+= i

@sp.add_test(name = "Third_test")
def test():
    scenario = sp.test_scenario()
    second_contract= Summarize()

    scenario+=second_contract
    scenario+= second_contract.sum()

That is, we could write a function, which is not dependent neither on the input or the storage. Keep in mind that either way it has write access to the storage.

How does the Michelson code of such a contract look like?

parameter unit;
storage   int;
code
  {
    CDR;        # @storage
    # == sum ==
    # self.data.storage += 1 # @storage
    PUSH int 1; # int : @storage
    ADD;        # int
    # self.data.storage += 2 # int
    PUSH int 2; # int : int
    ADD;        # int
    # self.data.storage += 3 # int
    PUSH int 3; # int : int
    ADD;        # int
    # self.data.storage += 4 # int
    PUSH int 4; # int : int
    ADD;        # int
    # self.data.storage += 5 # int
    PUSH int 5; # int : int
    ADD;        # int
    NIL operation; # list operation : int
    PAIR;       # pair (list operation) int
  };

Can you see how the loop is unrolled and Python appends the addition in-line?

In the Python code, you can see:

def sum(self):
        for i in range(1, 6):
            self.data.storage+= i

This will be translated to 5 times ADD in the Michelson code.

Remember that computation is costly on blockchains as each calculation costs gas and with it de facto money. A code with a minimalistic approach, regarding the contract code, is therefore cheaper. It is advisable to do the least possible number of calculations on the blockchain, metaprogramming can help with that.

If we instead use SmartPy and write:

import smartpy as sp

# Define Smartcontract

class Summarize(sp.Contract):
    def __init__(self):
        # Define storage with initial integer 0
        self.init(storage=0)

    # Define Entrypoint
    @sp.entry_point
    def sum(self):
        sp.for i in sp.range(1, 6):
            self.data.storage+= i

@sp.add_test(name = "Third_test")
def test():
    scenario = sp.test_scenario()
    second_contract= Summarize()

    scenario+= second_contract
    scenario+= second_contract.sum()

We get as Michelson code:

parameter unit;
storage   int;
code
  {
    DUP;        # pair @parameter @storage : pair @parameter @storage
    CDR;        # @storage : pair @parameter @storage
    SWAP;       # pair @parameter @storage : @storage
    CAR;        # @parameter : @storage
    # == sum ==
    # for i in sp.range(1, 6): ... (sp.TIntOrNat) # @parameter : @storage
    PUSH int 1; # int : @parameter : @storage
    PUSH bool True; # bool : int : @parameter : @storage
    LOOP
      {
        # self.data.storage += i # int : @parameter : @storage
        DIG 2;      # @storage : int : @parameter
        SWAP;       # int : @storage : @parameter
        DUP;        # int : int : @storage : @parameter
        DUG 2;      # int : @storage : int : @parameter
        ADD;        # int : int : @parameter
        DUG 2;      # int : @parameter : int
        # loop step # int : @parameter : int
        PUSH int 1; # int : int : @parameter : int
        ADD;        # int : @parameter : int
        DUP;        # int : int : @parameter : int
        PUSH int 6; # int : int : int : @parameter : int
        COMPARE;    # int : int : @parameter : int
        GT;         # bool : int : @parameter : int
      }; # int : @parameter : @storage
    DROP 2;     # @storage
    NIL operation; # list operation : @storage
    PAIR;       # pair (list operation) @storage
  };

Here, we have fewer ADD instructions, which are nested in the LOOP instruction because of:

    sp.for i in sp.range(1, 6):
            self.data.storage+= i

This means that the loop as such is included in Michelson. In doing so, we can see and decide which calculations we want to perform on the blockchain.

Typing

We know that Python uses duck typing and Michelson is strong typed. How does this fit together?

Let us take another look at our repeater contract:

# Import SmartPy
import smartpy as sp

# Define Smartcontract

class Repeater(sp.Contract):
    def __init__(self):
        # Define storage with initial integer 0
        self.init(storage=0)

    # Define Entrypoint
    @sp.entry_point
    def repeat(self, params):
        self.data.storage= params

@sp.add_test(name = "First_test")
def test():
    first_contract= Repeater()
    scenario = sp.test_scenario()

    scenario.register(first_contract, show = True)
    scenario+= first_contract.repeat(2)

When we take a more detailed look in the SmartPy editor under the tab Types, we see:

Storage:
{
  storage: sp.TIntOrNat;
}.layout("storage")
Entrypoints:
| repeat sp.TIntOrNat

Our storage has the type TIntOrNat. This is because Michelson has the types int and nat, and our script doesn't allow SmartPy to simply recognize which one we need or respectively which one we want to use. When observing the Michelson code, we can see that our storage, in the end, has the type int - eventually, a decision has to be made.

In case we initialise the storage differently:

        # Define storage with initial integer 0
        self.init(storage=sp.nat(0))

We see in the Types tab:

Storage:
{
  storage: sp.TNat;
}.layout("storage")
Entrypoints:
| repeat sp.TNat

Feel free to test:

        # Define storage with initial integer 0
        self.init(storage=sp.int(0))

And check which type we now have.

Conditions

Often we want a smart contract to only accepts a transaction under certain conditions. The most important tool in SmartPy for this is sp.verify(condition). Should the condition not be met, the entire transaction becomes invalid and our script stops running:

# Import SmartPy
import smartpy as sp

# Define Smartcontract

class Repeater(sp.Contract):
    def __init__(self):
        # Define storage with initial integer 0
        self.init(storage=sp.int(0))

    # Define Entrypoint
    @sp.entry_point
    def repeat(self, params):
        sp.verify(params>0)
        self.data.storage= params

@sp.add_test(name = "First_test")
def test():
    first_contract= Repeater()
    scenario = sp.test_scenario()

    scenario.register(first_contract, show = True)
    scenario+= first_contract.repeat(-1).run(valid=False)

In the Michelson code, you see:

parameter int;
storage   int;
code
  {
    CAR;        # @parameter
    # == repeat ==
    # sp.verify(params > 0) # @parameter
    DUP;        # @parameter : @parameter
    PUSH int 0; # int : @parameter : @parameter
    COMPARE;    # int : @parameter
    LT;         # bool : @parameter
    IF
      {}
      {
        PUSH string "WrongCondition: params > 0"; # string : @parameter
        FAILWITH;   # FAILED
      }; # @parameter
    # self.data.storage = params # @parameter
    NIL operation; # list operation : @parameter
    PAIR;       # pair (list operation) @parameter
  };

And because of our test with -1you can see the corresponding message in the editor:

editormessage

We also have sp.if. So, when we don't want our script to stop, we can write:

# Import SmartPy
import smartpy as sp

# Define Smartcontract

class Repeater(sp.Contract):
    def __init__(self):
        # Define storage with initial integer 0
        self.init(storage=sp.int(0))

    # Define Entrypoint
    @sp.entry_point
    def repeat(self, params):
        sp.if params>0:
            self.data.storage= params
        sp.else:
            self.data.storage= 0
            
@sp.add_test(name = "First_test")
def test():
    first_contract= Repeater()
    scenario = sp.test_scenario()

    scenario.register(first_contract, show = True)
    scenario+= first_contract.repeat(-1)

We have used sp.for already.

In addition, SmartPy offers sp.while.

    # Define Entrypoint
    @sp.entry_point
    def repeat(self, params):
        sp.while (params>0):
            params+=1
            
        self.data.storage= params
tip icon

Take a look at this SmartPy Reference Manual. It contains a very useful overview.

reading icon
Discuss on Slack
Rate this Page
Would you like to add a message?
Submit
Thank you for your Feedback!