Contracts
A SmartPy contract is a Python class that inherits from sp.Contract
. They can contain any number of entrypoints, views, and auxiliary functions.
Storage
Smart contracts can have any number of storage fields. Only the execution of the contract's entrypoints and __init__()
method can change the value of these fields.
Most contracts use an __init__()
method to initialise the contract storage. All storage fields must be initialised in this function; entrypoints can modify the storage but cannot add storage fields that were not initialised.
To initalise the contract storage, assign fields to the variable self.data
, as in this example:
class A(sp.Contract):
def __init__(self):
self.data.x = 0
@sp.entrypoint
def set_x(self, x):
self.data.x = x
To make the contents of the contract storage clear, you can cast the storage parameters to a type, as in this example:
@sp.module
def main():
storage: type = sp.record(
nat_value = sp.nat,
int_value = sp.int,
string_value = sp.string,
)
class B(sp.Contract):
def __init__(self, param):
self.data.nat_value = 0
self.data.int_value = param.int_value
self.data.string_value = param.string_value
sp.cast(self.data, storage)
This __init__()
method becomes the constructor to create an instance of the contract. In this way, you can use the method to pass storage values when you create an instance of the contract or deploy the contract, as in this example:
@sp.module
def main():
class A(sp.Contract):
def __init__(self, int_value, string_value):
self.data.int_value = sp.cast(int_value, sp.int)
self.data.string_value = sp.cast(string_value, sp.string)
@sp.add_test()
def test():
scenario = sp.test_scenario("A", main)
contract = main.A(12, "Hello!")
scenario += contract
scenario.verify(contract.data.int_value == 12)
scenario.verify(contract.data.string_value == "Hello!")
The __init__()
method can be declared with an effects specification using the @sp.init_effects
decorator.
Entrypoints and views can access values from storage with the self
object, which is their first parameter. This object has two fields:
self.data
: A variable that provides access to the values in the contract storage. You can set initial values in the__init__()
method and access and change them in entrypoints, as in this example:smartpyimport smartpy as sp @sp.module def main(): class MyContract(sp.Contract): def __init__(self, int_value, string_value): self.data.int_value = sp.cast(int_value, sp.int) self.data.string_value = sp.cast(string_value, sp.string) @sp.entrypoint def changeValues(self, newInt, newString): self.data.int_value = newInt self.data.string_value = newString
self.private
: A variable that provides access to constants and private lambdas.Constants behave like the storage values in
self.data
but only the__init__()
method can set them. They are read-only to all other code.
Metadata
Contracts can have metadata that provides descriptive information about them to wallets, explorers, dApps, and other off-chain applications. Contract metadata is stored off-chain and therefore on-chain applications including smart contracts cannot access it. To store data off-chain in a decentralized way, many Tezos developers use IPFS.
The primary Tezos standard for metadata is TZIP-016 (Tezos Metadata Standard).
Contracts store a link to their metadata in a big map of type sp.big_map[sp.string, sp.bytes]
. This big map is stored in a variable named metadata
in the contract storage. This big map always contains the empty key ""
and its value is an encoded URI that points to a JSON document.
SmartPy includes tools to help you create standard-compliant metadata and store it in IPFS. You create and publish contract metadata in a test scenario; see Creating and publishing metadata.
Inheritance
Contracts can inherit from each other as a superclass using the ordinary Python syntax:
class A(sp.Contract):
def __init__(self, x):
self.data.x = x
class B(A):
def __init__(self, x, y):
A.__init__(self, x)
self.data.y = y
Inheritance order
Attributes are first searched for in the current class. If not found, the search moves to parent classes. This is left-to-right, depth-first.
Initialization order
In SmartPy you must call the superclass's __init__()
method explicitly.
The order of initialization in SmartPy follows the order in which the __init__()
methods are called and the sequence in which fields are set. If a field is assigned multiple times during initialization, the last assignment is what determines the field's final value.
Passing parameters
Just like ordinary Python functions, you can define any number of parameters on SmartPy functions. However, the way callers pass those parameters is different depending on the number of parameters.
If a function accepts a single parameter, you can pass a literal value. For example, this contract has an entrypoint that accepts a single number as a parameter and sets its storage to the square of that number:
@sp.module
def main():
class Calculator(sp.Contract):
def __init__(self):
self.data.value = 0
@sp.entrypoint
def setSquare(self, a):
self.data.value = a * a
@sp.add_test()
def test():
scenario = sp.test_scenario("Calculator", main)
contract = main.Calculator()
scenario += contract
# There is one parameter, so pass a single value
contract.square(4)
scenario.verify(contract.data.value == 16)
If a function accepts multiple parameters, you pass them as a record. For example, this contract has an entrypoint that has two named parameters. To call it, the test code passes a record with two fields, one for each named parameter:
@sp.module
def main():
class Calculator(sp.Contract):
def __init__(self):
self.data.value = 0
@sp.entrypoint
def multiply(self, a, b):
self.data.value = a * b
@sp.add_test()
def test():
scenario = sp.test_scenario("Calculator", main)
contract = main.Calculator()
scenario += contract
# There are multiple parameters, so pass a record
contract.multiply(a=5, b=6)
scenario.verify(contract.data.value == 30)
If the function is within a SmartPy module, such as an auxiliary function, you must explicitly pass a record, as in this example:
@sp.module
def main():
def addInts(a, b):
sp.cast(a, sp.int)
return a + b
class Calculator(sp.Contract):
def __init__(self):
self.data.value = 0
@sp.entrypoint
def add(self, a, b):
self.data.value = addInts(sp.record(a = a, b = b))
These rules apply to all functions in a SmartPy module, including auxiliary functions, entrypoints, and views.
Auxiliary functions
Modules can contain auxiliary functions that you can use in contracts, as in this example:
@sp.module
def main():
def multiply(a, b):
return a * b
class C(sp.Contract):
@sp.entrypoint
def ep(self):
assert multiply(a=4, b=5) == 20
Auxiliary functions can be declared with an effects specification using the @sp.effects
decorator.
Entrypoints
Contracts can have any number of entrypoints, each decorated as sp.entrypoint
. Each entrypoint receives the self
variable as the first parameter, which provides access to the storage in the self.data
and self.private
records. Just like ordinary Python functions, you can define any number of other parameters on entrypoints.
Entrypoints can change values in the contract's storage but they cannot return a value.
An entrypoint may run logic based on:
- The contract storage
- The parameters that senders pass
- Transaction context values such as
sp.balance
andsp.sender
- The table of constants
Entrypoints cannot access information outside of Tezos, such as calling external APIs. If an entrypoint needs information from outside Tezos it must use oracles; see Oracles on docs.tezos.com and Using and trusting Oracles on opentezos.com.
Entrypoints have default effects for allowing changes to the contract storage, raising exceptions, permitting mutez calculations that may overflow or underflow and emitting new operations that are run after the entrypoint completes.
The default effect values can be changed by changing the appropriate fields in the @sp.entrypoint
decorator.
An entrypoint can call other entrypoints in its contract or entrypoints in other contracts.
For example, this contract has a single entrypoint that sets the value of a field in the contract storage:
class A(sp.Contract):
def __init__(self):
self.data.x = 0
@sp.entrypoint
def set_x(self, x):
self.data.x = x
Views
Views are a way for contracts to expose information to other contracts and to off-chain consumers.
A view is similar to an entrypoint, with a few differences:
- Views return a value.
- Calls to views are synchronous, which means that contracts can call views and use the returned values immediately. In other words, calling a view doesn't produce a new operation. The call to the view runs immediately and the return value can be used in the next instruction.
- Calling a view doesn't have any effect other than returning that value or raising an exception. In particular, it doesn't modify the storage of its contract and doesn't generate any operations.
- Views do not include the transfer of any tez and calling them does not require any fees.
There are two kinds of views:
- On-chain views have code in the smart contract itself
- Off-chain views have code in an off-chain metadata file
Views have default effects for reading the contract storage, raising exceptions, permitting mutez calculations that may overflow or underflow. Views are not allowed to write to storage or emit operations.
The default effect values can be changed by changing the appropriate fields in the @sp.onchain_view
or @sp.offchain_view
decorators.
Creating views
The @sp.onchain_view
annotation creates an on-chain view. For example, this contract has a view that returns a value from a big map in storage:
@sp.module
def main():
storage_type: type = sp.big_map[sp.address, sp.nat]
class MyContract(sp.Contract):
def __init__(self):
# Start with an empty big map
self.data = sp.big_map()
sp.cast(self.data, storage_type)
@sp.entrypoint
def add(self, addr, value):
# Add or update an element in the big map
currentVal = self.data.get(addr, default=0)
self.data[addr] = currentVal + value
@sp.onchain_view
def getValue(self, addr):
# Get a value from the big map
return self.data.get(addr, default=0)
The @sp.offchain_view
annotation creates an off-chain view. The code of an off-chain view can be the same as an on-chain view, but to enable it you must publish its code to an external source such as IPFS. See Creating and publishing metadata.
Views can't return values before the end of their code; they must return values as their last command. For example, this code is not valid because it could return from more than one place in the code, even though the return
statements are close to each other:
if a > b:
return a # Error: 'return' in non-terminal position.
return b
Instead, the compiler needs a single end block that returns a value, as in the previous example.
Calling views
- sp.view(view_name, address: sp.address, arg: t, return_type: type) → sp.option[return_type]
Calls the view
view_name
onaddress
giving the argumentarg
and expected return typereturn_type
. ReturnsNone
if no view exists for the given elements andsp.Some(return_type)
value.The
view_name
parameter must be a constant string.For example:
smartpyx = sp.view( "get_larger", sp.self_address(), sp.record(a=a, b=b), sp.int ).unwrap_some()
The method that calls a view will be required to have
(with_exceptions=True, with_mutez_overflow=True, with_mutez_underflow=True)
.
Private functions
Contracts can contain private functions that can be used in entrypoints and views of the same contract, as in this example:
@sp.module
def main():
class C(sp.Contract):
@sp.private
def multiply(self, a, b):
return a * b
@sp.entrypoint
def ep(self):
assert self.multiply(a=4, b=5) == 20
Each private method has access to the self object and can be declared with an effects specification using the @sp.effects
decorator.
Order of operations
When entrypoints call auxiliary functions or views, those calls run synchronously; the code of the entrypoint pauses, waits for the response, and continues.
However, if an entrypoint creates operations, such as a transfer of tez or a call to another entrypoint, even an entrypoint in the same contract, those operations run only after the entrypoint code completes.
For more information, see Operations and Operations on docs.tezos.com.
The self
object
Each method inside a contract receives the self
object as its first parameter. This object has two fields:
self.data
: A record that provides access to the contract storage. You can set initial values in the__init__()
method and access and change them in entrypoints, as in this example:smartpyimport smartpy as sp @sp.module def main(): class MyContract(sp.Contract): def __init__(self, intValue, stringValue): self.data.intValue = sp.cast(intValue, sp.int) self.data.stringValue = sp.cast(stringValue, sp.string) @sp.entrypoint def changeValues(self, newInt, newString): self.data.intValue = newInt self.data.stringValue = newString
self.private
: A record that provides access to constants and private lambdas.Constants behave like the storage fields in
self.data
but only the__init__()
method can set them. They are read-only to all other methods.
Deploying contracts
SmartPy doesn't include a built-in way to deploy (originate) contracts to Tezos. You can deploy contracts in the SmartPy IDE or use other tools that can deploy Tezos contracts.
One popular tool is the Octez client, which is a command-line client that runs many different kinds of Tezos operations. For installation instructions, see Installing the Octez client on docs.tezos.com.
Here are general steps for deploying a SmartPy contract with the Octez client:
Install SmartPy locally as described in Installation.
Install the Octez client and configure it for the target Tezos network. For example, to configure the client for the Ghostnet test network, get the URL of a Ghostnet node from https://teztnets.com and use it in the
octez-client config update
command, as in this example:bashoctez-client --endpoint https://rpc.ghostnet.teztnets.com config update
If you are using a testnet, Octez shows a warning that you are not using Mainnet. You can hide this message by setting the
TEZOS_CLIENT_UNSAFE_DISABLE_DISCLAIMER
environment variable to "YES".In the Octez client, create or import a wallet as described in Creating accounts.
Fund the wallet with tez tokens. If you are using a testnet, you can use the network's faucet to get free tez. Testnet faucets are listed on https://teztnets.com.
Compile the contract with the
python
command. SmartPy writes the output to a folder with the same name as the test scenario.In the output folder, find the compiled contract file, which ends in
contract.tz
. For example, if you downloaded and compiled the Welcome contract described in Installation, the compiled contract is in the fileWelcome/step_002_cont_0_contract.tz
.Optional: In the output folder, find the compiled initial storage value, which ends in
storage.tz
. For example, if you downloaded and compiled the Welcome contract described in Installation, the compiled contract storage value is in the fileWelcome/step_002_cont_0_storage.tz
.Use the
octez-client originate contract
command to deploy the contract. For example, this command deploys the Welcome contract and sets its initial storage:bashoctez-client originate contract welcome transferring 0 from my_account \ running Welcome/step_002_cont_0_contract.tz \ --init "`cat Welcome/step_002_cont_0_storage.tz`" --burn-cap 1
Now you can call the contract from a block explorer, a dApp, or the Octez client.