Skip to content

Commit

Permalink
Updated views topic (#328)
Browse files Browse the repository at this point in the history
* Views don't transfer tez

* First draft of a new topic on views

* Simplify

* Coding views in SmartPy

* Calling views from smartpy

* date

* clarify

* calling views with taquito

* octez-client typo

* Rewrite intro based on Mathias's suggestions

* Better examples

* headings

* code comments

* Don't need to pass unit; I tried this

* Rework sections

* Can't cause side effects

* as a convenient way to get information from a smart contract

* DEXs and liquidity pools is redundant

* on-chain and off-chain views

* Update link to octez docs

* off-chain users can ask a node to run a view and return the result immediately

Co-authored-by: NicNomadic <[email protected]>

* As -> Because

* better example of smartpy view

* Better name for view

* Typo

* Remove sync info and mention the octez client

---------

Co-authored-by: NicNomadic <[email protected]>
  • Loading branch information
timothymcmackin and NicNomadic authored Apr 11, 2024
1 parent d4150b3 commit eab7bd1
Showing 1 changed file with 210 additions and 72 deletions.
282 changes: 210 additions & 72 deletions docs/smart-contracts/views.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,86 +2,224 @@
title: Views
authors: 'Mathias Hiron (Nomadic Labs), Sasha Aldrick (TriliTech), Tim McMackin (TriliTech)'
last_update:
date: 5 October 2023
date: 2 April 2024
---

Views are a way for contracts to expose information to other contracts.

A view is similar to an entrypoint, with a few differences:

- Views return a value.
- 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.
In particular, it doesn't modify the storage of its contract and doesn't generate any operations.

## Example View

Here is an example that uses a view.
The following contract is a ledger that handles a fungible token and keeps track of how many tokens are owned by each user.

{<table>
<caption>Ledger contract</caption>
<thead>
<tr>
<th>Storage</th>
<th>Entrypoint effects</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<ul>
<li><code>ledger</code>: <code>big-map</code>
<ul>
<li>Key:<ul>
<li><code>user</code>: <code>address</code></li>
</ul>
</li>
<li>Value:<ul>
<li><code>tokens</code>: <code>nat</code></li>
</ul>
</li>
</ul>
</li>
</ul>
</td>
<td>
<ul>
<li><code>view getBalance(user: address)</code>
<ul>
<li>return <code>ledger[user].tokens</code></li>
</ul>
</li>
<li><code>transfer(nbTokens, destination)</code>
<ul>
<li>Check that <code>tokens[caller].tokens &gt;= nbTokens</code></li>
<li>Create an entry <code>tokens[destination]</code> (<code>value = 0</code> if it doesn't exist)</li>
<li>Add <code>nbTokens</code> to <code>tokens[destination].nbTokens</code></li>
<li>Subtract <code>nbTokens</code> from <code>tokens[caller].nbTokens</code></li>
</ul>
</li>
</ul>
</td>
</tr>
</tbody>
</table>}

Another contract might provide an `equalizeWith` entrypoint such that if they have more tokens than another user, they can make their balances equal (plus or minus one if the total amount is odd).
The following example code for this contract takes advantage of the `getBalance(user)` view of the first contract: to determine the balance of each user:
Views are a way for contracts to expose information to other contracts and to off-chain consumers.

Views help you get around a limitation in smart contracts: a smart contract can't access another contract's storage.
Smart contracts can provide information via callbacks, but using a callback means calling entrypoints, which is an asynchronous action.

By contrast, views are synchronous; a contract can call a view and use the information that it returns immediately.

Like entrypoints, views can accept parameters, access the contract's storage, and call other views.
Unlike entrypoints, views return a value directly to the caller.
However, views can't cause side effects, so they can't create operations, including calling smart contracts and transferring tez.
Views also can't change the contract storage.

Off-chain users can run a view without creating a transaction, which is a convenient way to get information from a smart contract.
For example, you can use the Octez client `run view` command to run a view from the command line.

## Types of views

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.

## Examples

Views can provide information about tokens.
You can use views to provide an account's balance of a token type or the total amount of a token in circulation.

DEXs can provide the exchange rate between two tokens or the amount of liquidity in the pool.

Instead of repeating certain logic in multiple places, you can put the logic in a view and use it from different smart contracts.

## Creating views in JsLIGO

Views in LIGO look like entrypoints because they receive the input values and storage as parameters, but they have the `@view` annotation instead of the `@entry` annotation.
They return a value instead of a list of operations and the new value of the storage.

This JsLIGO view returns the larger of two numbers:

```ts
type get_larger_input = [int, int];

@view
const get_larger = (input: get_larger_input, _s: storage): int => {
const [a, b] = input;
if (a > b) {
return a;
}
return b;
}
```

This view returns a value from a big-map in storage:

```ts
type storageType = big_map<string, string>;

@view
const get_balance = (key: string, s: storageType): string => {
const valOpt = Big_map.find_opt(key, s);
return match(valOpt) {
when(Some(val)): val;
when(None): "";
}
}
```

## Calling views in JsLIGO

This JsLIGO code calls the `get_larger` view from the previous example by passing the target contract address, parameters, and view name to the `Tezos.call_vew()` function:

```ts
@entry
const callView = (_i: unit, _s: storage): return_type => {
const resultOpt: option<int> = Tezos.call_view(
"get_larger", // Name of the view
[4, 5], // Parameters to pass
"KT1Uh4MjPoaiFbyJyv8TcsZVpsbE2fNm9VKX" as address // Address of the contract
);
return match(resultOpt) {
when (None):
failwith("Something went wrong");
when (Some(result)):
[list([]), result];
}
}
```

If the view takes no parameters, pass a Unit type for the parameter:

```ts
const unitValue: unit = [];
const resultOpt: option<int> = Tezos.call_view(
"no_param_view", // Name of the view
unitValue, // No parameter
"KT1Uh4MjPoaiFbyJyv8TcsZVpsbE2fNm9VKX" as address // Address of the contract
);
```

## Creating views in SmartPy

Views in SmartPy look like entrypoints because they receive the `self` object and input values as parameters, but they have the `@sp.onchain_view` annotation instead of the `@sp.entrypoint` annotation.

This SmartPy contract has a view that returns a value from a big-map in storage:

```python
@sp.module
def main():

storage_type: type = sp.big_map[sp.address, sp.nat]

class MyContract(sp.Contract):
def __init__(self):
self.data = sp.big_map()
sp.cast(self.data, storage_type)

@sp.entrypoint
def add(self, addr, value):
currentVal = self.data.get(addr, default=0)
self.data = sp.update_map(addr, sp.Some(currentVal + value), self.data)

@sp.onchain_view
def getValue(self, addr):
return self.data.get(addr, default=0)

@sp.add_test()
def test():
scenario = sp.test_scenario("Callviews", main)
contract = main.MyContract()
scenario += contract

alice = sp.test_account("Alice")
bob = sp.test_account("Bob")

# Test the entrypoint
contract.add(addr = alice.address, value = 5)
contract.add(addr = alice.address, value = 5)
contract.add(addr = bob.address, value = 4)
scenario.verify(contract.data[alice.address] == 10)
scenario.verify(contract.data[bob.address] == 4)

# Test the view
scenario.verify(contract.getValue(alice.address) == 10)
scenario.verify(contract.getValue(bob.address) == 4)
```

## Calling views in SmartPy

In SmartPy tests, you can call views in the contract just like you call entrypoints.
However, due to a limitation in SmartPy, if the view accepts multiple parameters, you must pass those parameters in a record.
For example, to call the `get_larger` view in the previous example, use this code:

```python
viewResult = contract.get_larger(sp.record(a = 4, b = 5))
scenario.verify(viewResult == 5)
```

To call a view in an entrypoint, pass the view name, target contract address, parameters, and return type to the `sp.view()` function, as in this example:

```python
@sp.entrypoint
def callView(self, a, b):
sp.cast(a, sp.int)
sp.cast(b, sp.int)
viewResponseOpt = sp.view(
"get_larger", # Name of the view
sp.address("KT1K6kivc91rZoDeCqEWjH8YqDn3iz6iEZkj"), # Address of the contract
sp.record(a=a, b=b), # Parameters to pass
sp.int # Return type of the view
)
if viewResponseOpt.is_some():
self.data.myval = viewResponseOpt.unwrap_some()
```

If the view takes no parameters, pass `()` for the parameter:

```python
viewResponseOpt = sp.view(
"no_param_view", # Name of the view
sp.address("KT1K6kivc91rZoDeCqEWjH8YqDn3iz6iEZkj"), # Address of the contract
(), # No parameter
sp.int # Return type of the view
)
```

## Calling views with Taquito

Calling a view with Taquito is similar to calling entrypoints.
When you create an object to represent the contract, its `contractViews` property has a method for each view, which you can call as in this example:

```javascript
equalizeWith(destination)
destinationBalance = ledger.getBalance(destination)
totalBalance = ledger.getBalance(caller) + destinationBalance
targetBalance = totalBalance // 2
ledger.transfer(targetBalance - destinationBalance, destination)
const viewContractAddress = "KT1K6kivc91rZoDeCqEWjH8YqDn3iz6iEZkj";
const contract = await Tezos.wallet.at(viewContractAddress);
const result = await contract.contractViews.get_larger({a: 2, b: 12})
.executeView({ viewCaller: viewContractAddress });
console.log(result);
```

## Calling views with the Octez client

To call a view with the Octez client, use the `run view` command, as in this example:

```bash
octez-client run view "get_larger" on contract "KT1Uh4MjPoaiFbyJyv8TcsZVpsbE2fNm9VKX" with input "Pair 4 5"
```

If the view takes no parameters, you can pass Unit or omit the `with input`.
<!-- TODO link to info on encoding param values -->

## Implementation details

- Michelson: [Operations on views](https://tezos.gitlab.io/active/michelson.html#operations-on-views)
- Octez: [On-chain views](https://tezos.gitlab.io/active/views.html)
- Archetype: [View](https://archetype-lang.org/docs/reference/declarations/view)
- SmartPy: [Views in testing](https://smartpy.io/manual/scenarios/testing_contracts#views)
- LIGO: [On-chain views](https://ligolang.org/docs/protocol/hangzhou#on-chain-views)
- Taquito: [On-chain views](https://tezostaquito.io/docs/on_chain_views)

0 comments on commit eab7bd1

Please sign in to comment.