Get data from "view" entrypoints
Often we need to access smart contract storage, to get specific data like token balance for a specific account, or total supply of some token, etc.
In Tezos there are two ways to get data from a smart contract:
- get the whole storage and try to find there raw data;
- use "view" entrypoints to get processed data.
Let's see how to work with "view" entrypoints (from the outside).
View entrypoints
A view
is an entrypoint that represents a computation that does not modify smart contract's state, but returns some result.
In Tezos view
entrypoints are actually the same entrypoints (technically), but following some conventions.
See more details here.
In contrast with view
functions in Ethereum, which are very easy to use, view
entrypoints in Tezos are much more complicated, because they do not return
a value directly, but pass it to the callback contract.
Callback contracts
A callback
is a contract (or an entrypoint of the contract, to be precise) which is used to process a result from the invoked view
entrypoint.
Obviously, a callback
must have the same type as the result from a view
entrypoint.
If you pass a result of type a
to a callback of type b
you will get a runtime error.
How it works
Let's see how it works in a real example.
This is a transaction that calls getBalance
view entrypoint with parameter tz1io...BtJKUP
and callback contract KT1Md...XUhn1
.
Note, by appending %viewNat
to the contract address we can specify a particular entrypoint of the contract that should be used as a callback.
{
"kind": "transaction",
"source": "...",
"destination": "KT1EwXFWoG9bYebmF4pYw72aGjwEnBWefgW5",
"parameters": {
"entrypoint": "getBalance", // call view entrypoint
"value": {
"prim": "Pair",
"args": [
{
"string": "tz1ioz62kDw6Gm5HApeQtc1PGmN2wPBtJKUP" // with some args
},
{
"string": "KT1MdfSC8xwRvxmd1UHANt158YtoFn4XUhn1%viewNat" // and callback contract
}
]
}
},
"metadata": {
"internal_operation_results": [ // callback transaction
{
"kind": "transaction",
"source": "KT1EwXFWoG9bYebmF4pYw72aGjwEnBWefgW5",
"destination": "KT1MdfSC8xwRvxmd1UHANt158YtoFn4XUhn1", // to callback contract
"parameters": {
"entrypoint": "viewNat", // to callback entrypoint
"value": {
"int": "37037036736" // with `getBalance` result
}
}
}
]
}
}
So, we called view
entrypoint by sending a transaction to a smart contract, then the smart contract produced
an internal transaction to the callback
contract with a result value in the parameters.
Call view entrypoints with Netezos
We've just seen how view
entrypoints work in Tezos: you send a transaction to the smart contract, then the smart contract produces
an internal transaction with result value in its parameters and sends it to the callback
contract.
With Netezos we will do basically the same things.
Let's see, how to get FA1.2 token
balance by calling getBalance
entrypoint.
Prepare callback contract
First of all, we will need an existing contract that we will use as a callback
.
Moreover, the callback
contract must have an entrypoint with the same parameter type as the type of the view
entrypoint we are calling.
Here is how the getBalance
entrypoint of our contract looks:
(pair %getBalance address (contract nat)))
As we can see, it takes an address
and returns nat
, that is passed to the callback contract.
So, we will need a callback
contract with an entrypoint of type nat
.
Where do we get such a contract? Well, we can either originate a new one or use any of already existing ones that have an entrypoint of the required type.
Let's take existing one:
parameter (or (nat %viewNat) (or (string %viewString) (address %viewAddress)));
storage unit;
code { FAILWITH }
That contract contains the entrypoint (nat %viewNat)
- that's exactly what we need.
Simulate operation
Of course, we don't want to send a transaction just to get some data from the smart contract. It would be weird to wait a minute unless a transaction is included into a block and moreover to pay a tx fee.
A common workaround is to use /run_operation RPC endpoint to simulate the transaction and see its result without injecting it into the blockchain, so we don't have to wait and we don't have to pay a fee. By the way, /run_operation ignores signature, so we don't even need to forge and sign the operation, just send its content.
Let's create a transaction:
var sender = "tz1ioz62kDw6Gm5HApeQtc1PGmN2wPBtJKUP";
var fa12 = "KT1EwXFWoG9bYebmF4pYw72aGjwEnBWefgW5";
var callback = "KT1MdfSC8xwRvxmd1UHANt158YtoFn4XUhn1%viewNat";
var rpc = new TezosRpc("https://rpc.tzkt.io/ghostnet/");
var counter = await rpc.Blocks.Head.Context.Contracts[sender].Counter.GetAsync<int>();
var tx = new TransactionContent
{
Source = sender,
Counter = ++counter,
GasLimit = 100_000,
Destination = fa12,
Parameters = new Parameters
{
Entrypoint = "getBalance",
Value = new MichelinePrim
{
Prim = PrimType.Pair,
Args = new List<IMicheline>
{
new MichelineString(sender),
new MichelineString(callback)
}
}
}
};
Nothing new, this is just a normal transaction with parameters. Then we simulate its execution via RPC:
var operation = await rpc.Blocks.Head.Helpers.Scripts.RunOperation.PostAsync(tx);
Extract result
And finally, we can parse the simulation result (operation) and, despite it has failed (because the callback contract does nothing but FAILWITH
), extract the getBalance
value from the internal transaction parameters:
var balance = operation
.contents[0]
.metadata
.internal_operation_results[0]
.parameters
.value
.@int;
// 37037036736
That's it. In a similar way we can call any other view
entrypoints.
Advanced: get FA2 balance
Another example, demonstrating more complex callback
type.
Prepare callback contract
In case of FA2 standard, balance_of
has the following type:
(pair %balance_of (list %requests (pair (address %owner) (nat %token_id)))
(contract %callback (list (pair
(pair %request (address %owner) (nat %token_id))
(nat %balance)))))
So we need a callback contract with an entrypoint of type:
(list (pair
(pair %request (address %owner) (nat %token_id))
(nat %balance)))))
This time let's originate our own contract that we will use as a callback
for all calls of FA2 balance_of
:
var origination = new OriginationContent
{
Source = sender,
Counter = ++counter,
GasLimit = 2000,
StorageLimit = 100,
Fee = 1000000,
Script = new Script
{
Code = Micheline.FromJson(@"
[{""prim"":""parameter"",""args"":[{""prim"":""list"",""args"":[{""prim"":
""pair"",""args"":[{""prim"":""pair"",""args"":[{""prim"":""address"",
""annots"":[""%owner""]},{""prim"":""nat"",""annots"":[""%token_id""]}],
""annots"":[""%request""]},{""prim"":""nat"",""annots"":[""%balance""]}]}]}
]},{""prim"":""storage"",""args"":[{""prim"":""unit""}]},{""prim"":""code"",
""args"":[[{""prim"":""FAILWITH""}]]}]") as MichelineArray,
Storage = new MichelinePrim { Prim = PrimType.Unit }
},
};
var bytes = await new LocalForge().ForgeOperationAsync(head, origination);
var sig = (byte[])key.SignOperation(bytes);
var op_hash = await rpc.Inject.Operation.PostAsync(bytes.Concat(sig));
// KT1Cm94oSUbLUZasuZo285uuw9YDzuZEJUY6
Now we can use KT1Cm94oSUbLUZasuZo285uuw9YDzuZEJUY6
as a callback contract (in your case it will be a different address).
Simulate operation
When we have prepared callback contract, we can simulate an operation:
var fa2 = "KT1DYk1XDzHredJq1EyNkDindiWDqZyekXGj";
var callback = "KT1Cm94oSUbLUZasuZo285uuw9YDzuZEJUY6";
var script = await rpc.Blocks.Head.Context.Contracts[fa2].Script.GetAsync();
var code = Micheline.FromJson(script.code);
var cs = new ContractScript(code);
var param = cs.BuildParameter(
"balance_of",
new
{
requests = new []
{
new
{
owner = sender,
token_id = 0
}
},
callback
});
var tx = new TransactionContent
{
Source = key.PubKey.Address,
Counter = ++counter,
GasLimit = 100_000,
StorageLimit = 257,
Fee = 100_000,
Destination = fa2,
Parameters = new Parameters
{
Entrypoint = "balance_of",
Value = param
}
};
var operation = await rpc.Blocks.Head.Helpers.Scripts.RunOperation.PostAsync(tx);
Extract result
In the same way as with FA1.2 we can extract the value from the simulation result:
var value = operation
.contents[0]
.metadata
.internal_operation_results[0]
.parameters
.value;
This is raw Micheline JSON:
[
{
"prim":"Pair",
"args":[
{
"prim":"Pair",
"args":[
{
"bytes":"0000fe2ce0cccc0214af521ad60c140c5589b4039247"
},
{
"int":"0"
}
]
},
{
"int":"12345678912"
}
]
}
]
We can also convert it to human-readable JSON:
var cbScript = await rpc.Blocks.Head.Context.Contracts[callback].Script.GetAsync();
var cbCode = Micheline.FromJson(cbScript.code);
var cbCs = new ContractScript(cbCode);
var json = cbCs.HumanizeParameter("default", Micheline.FromJson(value));
So in the end we will get:
[
{
"request":{
"owner":"tz1ioz62kDw6Gm5HApeQtc1PGmN2wPBtJKUP",
"token_id":"0"
},
"balance":"12345678912"
}
]
That's it! :D