Starting with SmartPy Part 2: Complex Constructions
Crafting Smart Contracts with Advanced Techniques
With the solid foundation of SmartPy fundamentals that we learned in Part 1, we can start creating advanced smart contracts that can store high-level data structures, perform run-time verification, and execute internal function calls.
Introduction To Maps
Maps are data structures that store key-value pairs. As a result, they enable smart contracts to create valuable associations between related information. In the NameRegistry
contract below, we define initial storage to contain a map
called addressToName
constructed by sp.map()
.
The register()
entry point allows a user to map their address, sp.sender
, to a given name, params.name
, with the expression self.data.<map>[<key>] = <value>
. Before the assignment statement, we set the type of params.name
to be a string
. The expression sp.setType(<var>, <type>)
is used to reduce ambiguity and enable SmartPy to inference variable types when compiling.
import smartpy as spclass NameRegistry(sp.Contract):
def __init__(self):
self.init(addressToName = sp.map()) @sp.entryPoint
def register(self, params):
sp.setType(params.name, str)
self.data.addressToName[sp.sender] = params.name
Putting It All Together
Ready for a real challenge? Let’s try creating an EventPlanner
smart contract that utilizes more advanced storage fields, entry points, and internal functions. Our EventPlanner
will allow its owner to keep track of multiple events and change information about each event.
Our first step is initializing storage with two fields, owner
and nameToEvent
. owner
is the Tezos address
of the EventPlanner
's owner, who is the only one capable of interacting with their instance of the smart contract. nameToEvent
is a map
that associates a string
event name with an sp.record
data structure containing date
and numGuests
as fields. The owner
is initialized to initialOwner
, while nameToEvent
is set as an empty map with sp.map()
.
import smartpy as spclass EventPlanner(sp.Contract):
def __init__(self, initialOwner):
self.init(owner = initialOwner,
nameToEvent = sp.map())
We continue by defining three entry points that will give EventPlanner
its functionality. changeDate()
and changeNumGuests()
enable the owner
to modify the date and number of guests for a given event. The params
of these two entry points contains an event name, name
, and the new information to be added, either newDate
as a string
or newNumGuests
as an int
. changeOwner()
is a simple transference entry point that checks if the sender is the owner
, and then sets the owner
to the given newOwner
address.
Both changeDate()
and changeNumGuests()
use the sp.verify()
method to check that sp.sender
, the address of the sender, matches self.data.owner
, the address of the contract’s owner. If the requirement isn’t fulfilled, the entry point will fail and all executed operations will be reversed. Then, they call self.checkEvent(params.name)
, which is an helper function that will create a new entry in nameToDate
if the event doesn’t already exist.
Subsequently, they use an assignment statement of the form self.data.<map>[<key>].<field> = params.<newValue>
to modify the values stored in the map
called nameToEvent
. In this example, key
is the name of the mapped event, field
is the record field to be modified, and newValue
is the parameter field that contains the value to be changed.
@sp.entryPoint
def setDate(self, params):
sp.verify(sp.sender == self.data.owner)
self.checkEvent(params.name)
self.data.nameToEvent[params.name].date = params.newDate @sp.entryPoint
def setNumGuests(self, params):
sp.verify(sp.sender == self.data.owner)
self.checkEvent(params.name)
self.data.nameToEvent[params.name].numGuests = params.newNumGuests @sp.entryPoint
def changeOwner(self, params):
sp.verify(sp.sender == self.data.owner)
self.data.owner = params.newOwner
Finally, we define our helper function checkEvent()
. Since this isn’t an entry point, we can define all of its parameters explicitly instead of referencing them as fields of params
. Employing the sp.if()
statement to define the contract’s control flow, this function checks if the event name
already exists within nameToEvent
. Otherwise, it maps name
to a new sp.record
, created with the sp.record()
constructor, that has date
and numGuests
as fields.
def checkEvent(self, name):
sp.if ~(self.data.nameToEvent.contains(name)):
self.data.nameToEvent[name] = sp.record(date = "", numGuests = 0)
With this last piece, our EventPlanner
smart contract is complete!
import smartpy as spclass EventPlanner(sp.Contract):
def __init__(self, initialOwner):
self.init(owner = initialOwner,
nameToEvent = sp.map(tkey = sp.TString)) @sp.entryPoint
def setDate(self, params):
sp.verify(sp.sender == self.data.owner)
self.checkEvent(params.name)
self.data.nameToEvent[params.name].date = params.newDate @sp.entryPoint
def setNumGuests(self, params):
sp.verify(sp.sender == self.data.owner)
self.checkEvent(params.name)
self.data.nameToEvent[params.name].numGuests = params.newNumGuests @sp.entryPoint
def changeOwner(self, params):
sp.verify(sp.sender == self.data.owner)
self.data.owner = params.newOwner def checkEvent(self, name):
sp.if ~(self.data.nameToEvent.contains(name)):
self.data.nameToEvent[name] = sp.record(date = "", numGuests = 0)
Feel free to experiment with the EventPlanner
by adding more information to each event record or creating new entry points to interact with the contract. Continue to Part 3 to learn more about contract simulation.