Skip to main content
Version: Next

Views

Views are a way for contracts to expose information to other contracts and to off-chain consumers. Like entrypoints, views are functions that receive a parameter and the current value of the contract storage. Unlike entrypoints, views return a value directly to the caller, and that value can be any type.

Clients can call views off-chain without paying fees or sending transactions to Tezos because clients can calculate the output of a view from the current state of the contract. However, when a contract calls a view on-chain as part of an operation, it increases the gas fee of the operation.

Contracts can store the source code of their views either on-chain or off-chain:

  • The code of on-chain views is stored in the smart contract code itself, like entrypoints.
  • The code of off-chain views is stored externally, usually in decentralized data storage such as IPFS. The contract metadata has information about its off-chain views that consumers such as indexers and other dApps use to know what off-chain views are available and to run them.

On-chain and off-chain views have the same capabilities and limitations.

For more information about views, see Views on docs.tezos.com.

Defining on-chain views

To define an on-chain view, use the @view decorator.

type storage = string
type ret = [list<operation>, storage];
@entry
const main = (word : string, storage : storage) : ret
=> [[] , storage + " " + word]
// This view returns the storage
@view
const view1 = (_arg : unit, storage : storage) : storage
=> storage;
// This view returns true if the storage has a given length
@view
const view2 = (expected_length : nat , storage : storage) : bool
=> (String.length (storage) == expected_length);
// This view does not use the parameters or storage and returns a constant int
@view
const view3 = (_arg : unit , _s : storage) : int
=> 42;

Defining off-chain views

To compile an off-chain view, create a function, compile it as an expression, and put the expression in the contract's metadata.

To compile an expression as a off-chain view, use the LIGO compile expression command and pass the --function-body flag. To use an expression from a file, pass it in the --init-file argument.

For example, this file has a contract named C with a function named v:

namespace C {
type storage = string
@entry
const append = (a: string, s: storage) : [list<operation> , storage] => [[], s + a];
@entry
const clear = (_p: unit, _s: storage) : [list<operation>, storage] => [[], ""];
export const v = (expected_length: nat, s: storage) : bool => (String.length (s) == expected_length);
}

To compile the function v as an off-chain view, pass C.v to the compile expression command, as in this example:

ligo compile expression jsligo "C.v" --init-file off_chain.jsligo --function-body

The response is the function compiled to Michelson. It is up to you to store this code and link to it from the contract metadata.

{ UNPAIR ; SWAP ; SIZE ; COMPARE ; EQ }

Note that the function is not annotated as an entrypoint or on-chain view; it is just a function declared in the context of the contract.

Calling views

Contracts can call on-chain and off-chain views with the Tezos.call_view function and use the result immediately.

const call_view : string => 'arg => address => option <'ret>

The function accepts these parameters:

  • The name of the view
  • The parameter to pass to the view
  • The address of the contract

For example, this contract has a view that multiplies the integer in storage with the integer that the caller passes and returns the result:

namespace ContractWithView {
type storage = int;
type return_type = [list<operation>, storage];
@entry
const main = (param: int, _storage: storage): return_type =>
[[], param];
@view
const multiply = (param: int, storage: storage): int =>
param * storage;
}

This contract stores the address of the first contract and calls its view:

namespace CallView {
type storage = [address, int];
type return_type = [list<operation>, storage];
@entry
const callView = (param: int, storage: storage): return_type => {
const [targetAddress, _s] = storage;
const resultOpt: option<int> = Tezos.call_view(
"multiply",
param,
targetAddress
);
return match(resultOpt) {
when (None):
failwith("Something went wrong");
when (Some(newValue)):
[[], [targetAddress, newValue]];
}
}
}

This test deploys both contracts, calls the contract that calls the view, and verifies the result:

const test = (() => {
// Originate ContractWithView
const contract1 = Test.Next.Originate.contract(contract_of(ContractWithView), 5, 0tez);
const addr1 = Test.Next.Typed_address.to_address(contract1.taddr);
// Originate CallView with address of ContractWithView in storage
const initial_storage = [addr1, 0 as int];
const contract2 = Test.Next.Originate.contract(contract_of(CallView), initial_storage, 0tez);
// Call callView
Test.Next.Contract.transfer_exn(Test.Next.Typed_address.get_entrypoint("default", contract2.taddr), 12, 0tez);
const [_address, integer] = Test.Next.Typed_address.get_storage(contract2.taddr);
Assert.assert(integer == 60);
}) ()