Casting
The SmartPy compiler must know the type of all variables in order to compile to Michelson, which is a strongly-typed language. SmartPy has powerful type inference, which can determine the types of most variables based on how they are used.
However, sometimes it is necessary to cast a variable with sp.cast
to specify its type. For example, the errors Error in generated Michelson contract: Missing type in storage
or Missing type in parameters
often mean that SmartPy can't determine the type of a variable in the contract storage or in an entrypoint parameter.
Other errors might lead you to think that you need to cast a variable when you don't. Errors that start with entrypoint expects parameter of type [type a], but got [type b]
usually mean that you are passing the wrong type of variable to an entrypoint.
INFO
In SmartPy, casting does not change the type of a variable, like casting does in some other languages. Instead, casting identifies the type of a variable that the compiler can't infer from context.
The STDLIB modules provide some traditional casting functions, such as converting between different numerical types.
- sp.cast(x, T) → T
Assign value
x
to typeT
.Casting can set the types of entrypoint parameters and resolve type ambiguities:
smartpy@sp.entrypoint def ep(x, y): sp.cast(x, sp.int) sp.cast(y, sp.nat) ...
You can cast values either as a statement or as an expression, as in these examples:
smartpy# Cast as an expression b = sp.cast(a, sp.int) + 1 # Cast as a statement c = 5 sp.cast(c, sp.nat)
When is casting not necessary?
Casting is not necessary when SmartPy can infer a variable type from the context. For example, casting is not necessary if you assign a value to a variable, because SmartPy can use the type of that value as the type of the variable, as in this example:
# No casting required
my_string = "hello" # Inferred to be sp.string
my_list = [my_string] # Inferred to be sp.list[string]
Casting is also not necessary when SmartPy can infer a type based on an operation. For example, this entrypoint accepts a parameter that is not casted, but SmartPy can infer that it is a nat because the entrypoint adds it to another nat:
@sp.entrypoint
def acceptANat(self, a):
b = sp.nat(5)
c = a + b # All variables assumed to be sp.nat
SmartPy can infer types in this way across entrypoints. For example, the following contract's __init__()
method creates an empty big map and its update
entrypoint adds an element to that big map. SmartPy can infer the type of the big map by the element that the entrypoint adds:
class MyContract(sp.Contract):
def __init__(self):
# Inferred to be sp.big_map[sp.string, sp.int] based on usage
self.data.my_big_map = sp.big_map()
@sp.entrypoint
def update(self):
self.data.my_big_map["a"] = 5
INFO
SmartPy does not use information from the test scenario to infer types. All type information comes from the contract code itself.
When does SmartPy assume types?
The SmartPy compiler sometimes assumes a variable's type when it does not have complete information.
When SmartPy knows that a variable is a number but not whether that number is an integer or a nat, it assumes that it is an integer. For example, this __init__()
function creates a storage field and puts a number in it. In the absence of other information, SmartPy assumes that it is an integer:
def __init__(self):
self.data.my_value = 5 # Assumed to be sp.int
Similarly, SmartPy assumes that key-value series in braces are maps, not big maps, as in this example:
def __init__(self):
# Assumed to be sp.map[sp.string, sp.int]
self.data.my_map = {'a': 1, 'b': 2}
When is casting necessary?
Because SmartPy can use many different ways of inferring types, casting is rarely necessary. Here are some cases that may require casting:
Setting the type of unused variables
Casting may be required during the development process when you create a variable but do not use it yet.
For example, this contract's __init__()
method creates an empty big map. If no entrypoints manipulate this big map directly, you may need to cast it temporarily to specify its type:
class MyContract(sp.Contract):
def __init__(self):
self.data.my_big_map = sp.big_map()
sp.cast(self.data.my_big_map, sp.big_map[sp.string, sp.nat])
Of course, completed contracts don't usually have unused variables in them. Therefore, casting is usually necessary only during the development process, before your code is complete.
Setting the layout for parameters
Casting also allows you to set the layout of records in storage or parameters. For example, the FA2 standard specifies a layout and annotations for entrypoint parameters. The transfer
entrypoint parameter receives this Micheline value:
(list %transfer
(pair
(address %from_)
(list %txs
(pair
(address %to_)
(pair
(nat %token_id)
(nat %amount)
)
)
)
)
)
To implement this layout in SmartPy and include the annotations, you can cast the fields in the parameter. The SmartPy FA2 library provides types for this purpose. In this case, the t.transfer_batch
type sets the fields and annotations for the transfer
entrypoint. These are the types:
tx: type = sp.record(
to_=sp.address,
token_id=sp.nat,
amount=sp.nat,
).layout(("to_", ("token_id", "amount")))
transfer_batch: type = sp.record(
from_=sp.address,
txs=list[tx],
).layout(("from_", "txs"))
transfer_params: type = list[transfer_batch]
Then, the transfer
entrypoint casts its parameter to the t.transfer_batch
type:
@sp.entrypoint
def transfer(self, batch):
"""Accept a list of transfer operations between a source and multiple
destinations.
`transfer_tx_` and `is_defined_` must be defined in the child class.
"""
sp.cast(batch, t.transfer_params)
# ...
You may need to set layouts in this way to accept calls from contracts that provide parameters in a specific layout.
When is casting helpful?
Casting can be helpful (but is not required) to set the type of storage or of a parameter. If you set a type in this way by casting, the SmartPy compiler warns you if the code deviates from this type. For example, this code creates a type for the storage with comments that describe what each field is for. Then the __init__()
method casts the storage to this type to ensure that the contract uses this type:
contract_storage_type: type = sp.record(
my_integer = sp.int, # An integer value
my_string = sp.string, # A string value
my_record = sp.record(
my_address = sp.address, # An address
my_nat = sp.nat, # A nat
my_bool = sp.bool, # A Boolean
my_list_nat = sp.list[sp.nat] # A list of nat
)
)
class MyContract(sp.Contract):
def __init__(self):
# Cast storage to alert on differences
sp.cast(self.data, contract_storage_type)
Type aliases
To help with casting, you can create type aliases to identify types. These types can be simple or complex.
- x : type = t
Alias a type.
smartpy@sp.module def main(): # my_integer_type is now equivalent to sp.int my_integer_type: type = sp.int # A more complex type for the contract storage contract_storage_type: type = sp.record( ledger = sp.big_map[sp.pair[sp.address, sp.nat], sp.nat], supply = sp.big_map[sp.nat, sp.nat], metadata = sp.big_map[sp.string, sp.bytes], token_metadata = sp.big_map[ sp.nat, sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]).layout( ("token_id", "token_info") ), ] ) class MyContract(sp.Contract): def __init__(self): self.data.ledger = sp.big_map() self.data.supply = sp.big_map() self.data.metadata = sp.big_map() self.data.token_metadata = sp.big_map() # Cast the storage to a type alias sp.cast(self.data, contract_storage_type) @sp.entrypoint def ep(x): # my_integer_type can be used anywhere you would use a type sp.cast(x, my_integer_type)