Views β
Several related but somewhat incompatible notions have been called views on Tezos.
From an historical perspective, we got:
- CPS Views: (legacy pattern) entrypoints that happen to compute something from their storage without changing the state and callbacks one of its two parameters;
- Off-chain Views: functions that look like on-chain views but are evaluated by an external process;
- On-chain Views: special entrypoints that cannot change their state by construction and are much cheaper to call.
SmartPy supports all these notions.
CPS Views β
See helpers/utils.
We don't expect this to be used anymore except in standards that contain them for legacy reasons such as FA1.2.
Off-chain Views β
Exactly like a on-chain view except that:
- the code is not generated inside the contract (but typically in metadata);
- the generated code doesn't contain parameters when their parameter type would have been
sp.TUnit
.
Defining an off-chain view β
@sp.offchain_view(pure=False,Β doc=None,Β name=None)
Decorator to build an off-chain view. This is typically used inside a contract class.
Argument | Description |
---|---|
pure | Declare the view as pure, default is False. |
doc | Add a custom documentation. Python docstrings are also handled. |
name | Override the Python name. |
@sp.offchain_view()
def get_cst(self):
"""42"""
sp.result(42)
@sp.offchain_view(doc = "My bad")
def big_fail(self):
sp.failwith("my_bad")
@sp.offchain_view()
def some_computation(self, params):
sp.result(12 * params + self.data.x)
See reference Metadata template.
On-Chain Views β
The feature specification can be found here.
They are synchronous calls, meaning that the result is immediately available on the stack of the caller contract, which will make the contract development more effortless.
Views are a mechanism for contract calls that:
- Are read-only: they may depend on the storage of the contract declaring the view but cannot modify it nor emit operations;
- Can call other views;
- Take arguments as input in addition to the contract storage;
- Return a result as output;
- Are synchronous: the result is immediately available on the stack of the caller contract.
In other words, the execution of a view is included in the operation of callerβs contract, but accesses the storage of the declarerβs contract, in read-only mode. Thus, in terms of execution, views are more like lambda functions rather than contract entrypoints.
See reference On-Chain Views template.
They can be freely used as off-chain views as well.
Defining an on-chain view β
@sp.onchain_view(pure=None,Β doc=None,Β name=None)
Decorator to build an off-chain view. This is typically used inside a contract class.
Argument | Description |
---|---|
pure | Declare the view as pure, default is False. |
doc | Add a custom documentation. Python docstrings are also handled. |
name | Override the Python name. |
By default, the view
name is equal to the method name, where <name>
is an optional argument and can be used to set a view name explicitly.
Examples β
# The view name will be "view1"
@sp.onchain_view()
def view1(self):
# sp.result is used to return the view result (the contract storage in this case)
sp.result(self.data)
# The view name will be "getState", it is being set explicitly
@sp.onchain_view(name = "getState")
def view2(self, param):
state = self.data.state[param];
sp.verify(state == 2, "The state is not equal to 2.")
sp.result(state)
Calling an on-chain view in a contract β
On-chain views are called with sp.view(<view_name>,Β <contract_address>,Β <argument>,Β tΒ =Β <return_type>)
, and their output is of type sp.TOption(<return_type>).
Arguments:
Argument | Required | Description | Examples |
---|---|---|---|
view_name | Yes | The name of the view being called. | "view1" , "getState" , "computeSomething" |
contract_address | Yes | The contract address where the view is defined. | sp.address("KT1TezoooozzSmartPyzzSTATiCzzzwwBFA1") |
argument | Yes | The view argument. | sp.unit , 10 , "Some Text" , sp.bytes("0x0123") |
return_type | No (It is optional) | The view return type. Not required when the view is known by the compiler. (e.g. Defined in the same contract.) | sp.TNat , "sp.TString" |
Examples β
import smartpy as sp
# A contract that serves as storage and provides information to other contracts through an on-chain view
class Provider(sp.Contract):
def __init__(self, tokens):
self.init(tokens = tokens)
@sp.onchain_view()
def getTokenById(self, tokenID):
sp.verify(self.data.tokens.contains(tokenID), "Token doesn't exist")
sp.result(self.data.tokens[tokenID])
# Contract that will call the view defined in the consumer above
class Consumer(sp.Contract):
@sp.entrypoint
def checkToken(self, params):
token = sp.view("getTokenById", params.providerAddress, params.tokenID, t = sp.TRecord(balance = sp.TNat)).open_some("Invalid view");
sp.verify(token.balance >= 10, "Token balance is lower than 10")
Testing an on-chain view β
Views can be called from test scenarios the same way as entrypoints my_contract.my_view(some_parameters)
. This has the additional benefit over sp.view(..)
to make a full use of type inference which prevents type errors.
The Michelson semantics for sp.view(..)
says that any type error yields to a sp.none
. This is rather difficult to debug so SmartPy provides two semantics:
- the regular one with flag "no-view-check-exception";
- an enhanced one with proper exceptions with flag flag "view-check-exception".
Examples β
See reference On-Chain Views and On-Chain Views Exceptions templates.
import smartpy as sp
class MyContract(sp.Contract):
def __init__(self, param):
self.init(param)
@sp.onchain_view()
def state(self, param):
sp.verify(param < 5, "This is false: param >= 5")
sp.result(self.data * param)
@sp.add_test(name = "Test")
def test():
scenario = sp.test_scenario()
c1 = MyContract(1)
scenario += c1
""" Test views """
# Display the view result
scenario.show(c1.state(1))
# Assert the view result
scenario.verify(c1.state(2) == 2)
# Assert call failures
scenario.verify(sp.is_failing(c1.state(6))); # Expected to fail
scenario.verify(~ sp.is_failing(c1.state(1))); # Not expected to fail
# Assert exception result
# catch_exception returns an option:
# sp.none if the call succeeds
# sp.some(<exception>) if the call fails
e = sp.catch_exception(c1.state(7), t = sp.TString)
scenario.verify(e == sp.some("This is false: param >= 5"))
class Test(sp.Contract):
def __init__(self):
self.init(value = 0)
@sp.offchain_view()
def other(self, x):
sp.result(1 + x)
if "templates" not in __name__:
@sp.add_test(name="Test_exceptions")
def test():
scenario = sp.test_scenario()
provider = sp.address("KT1")
scenario.verify(sp.catch_exception(sp.view("a_view", provider, 0, t = sp.TNat)) == sp.some("Missing contract for view"))
c1 = Test()
scenario += c1
scenario.verify(c1.other(42) == 43)
scenario.verify(sp.view("other", c1.address, 1, t = sp.TIntOrNat) == sp.some(2))
scenario.verify(sp.catch_exception(sp.view("other", c1.address, 1, t = sp.TInt) == sp.some(2)) == sp.some("Type error in view"))
scenario.verify(sp.catch_exception(sp.view("missing_view", c1.address, 1)) == sp.some("Missing view missing_view in contract KT1TezoooozzSmartPyzzSTATiCzzzwwBFA1"))
@sp.add_test(name="Test_view_no_exception")
def test2():
scenario = sp.test_scenario()
scenario.add_flag("no-view-check-exception")
provider = sp.address("KT1")
scenario.verify(sp.view("a_view", provider, 0, t = sp.TNat) == sp.none)
c1 = Test()
scenario += c1
scenario.verify(c1.other(42) == 43)
scenario.verify(sp.view("other", c1.address, 1, t = sp.TIntOrNat) == sp.some(2))
scenario.verify(sp.view("other", c1.address, 1) == sp.some(2))
scenario.verify(sp.view("missing_view", c1.address, 1) == sp.none)
scenario.verify(sp.view("other", c1.address, 1, t = sp.TInt) == sp.none)