This is a technical suggestion (+ proof of concept) of doing FA token accounting by indexers.
Reasons
There are two separate issues while indexing token operations:
- We need some basic metadata to display balances, at least
symbol
and number ofdecimals
; - In
FA1.2
and upcomingFA2
standards thetransfer
method is standardized, but there are other methods that alter token balances such asmint
andburn
; thus we cannot do accounting based ontransfer
call parameters only.
What we need
A lightweight, and preferably stateless solution that could derive token balance updates from just RPC output ( /chains/main/blocks/{block_id}
). This would allow to add generic token support to the standard indexer workflow at minimal cost. Ideally, it should work like the balance_updates
receipts.
Big Map Diff
It can be argued that the vast majority of contracts implementing FA-family standards are/will store its balance ledgers in lazy structures aka Big_map
. Thus, the big_map_diff
data (list of changed Big_map
keys with new values) that is being attached to every operation is actually what we need. The only problem is that different contracts have different storage types and we need to know exactly how to extract the data we need.
Agnostic to standards
Using big_map_diff data as a calculation basis gives us a relative independence from the token standards. It doest matter whether transfer / mint / burn / other method was called - in the end we get just a list of balances for each token holder affected.
Custom handlers
As mentioned above, in addition to FA-standardized methods, there may be other methods that modify user balances and it is impossible to standardize them all, plus we would have to sacrifice flexibility.
This means that indexer developers have to implement custom handlers for each new FA token (this is how it works at the moment). It’s not good for many reasons, and there should be another solution.
Michelson scripts/plugins
The idea of using Michelson as a generic script language has been voiced several times [1], [2] and seems like a way out. But the following question arises: how to execute these scripts. Of course you can use the standard RPC run_code
endpoint but it’s costly and suboptimal.
Luckily, there’s an ongoing work on encapsulating the Michelson script interpreter and hopefully we will be able to link it as a standalone library in the future. Moreover, there are several other Michelson implementations (in Haskell by Serokell, in Python by Baking Bad, probably more), and since we need only a relatively small subset of instructions (no blockchain bindings which are most costly to implement) it’s a rather doable task to write an own, or to make a collaborative effort.
NOTE: You can actually write a parser script in any high-level language you want (LIGO, SmartPy, Lorentz, SCaml, etc.) since it’s a valid Tezos contract and can be compiled down.
Possible Shell integration
Another alternative to writing own interpreter is to integrate this plugin system into the Tezos Shell. It would require to register custom big_map_diff
parsers by the node operator, e.g. like that:
tezos-node register big_map_diff handler "tzbtc_parser.tz" for big_map 31
From the POV of a developer it could look like an RPC response extension, for each supported Big_map
he would receive an extra operation receipt:
...
"operation_result": {
"balance_updates": [{
"kind": "token",
"address": "tz1d75oB6T4zUMexzkr5WscGktZ1Nss1JrT7",
"balance": 100500,
"symbol": "TZBTC",
"decimals": 8
},
... ]
},
...
Other questions
Who will write parser scripts
Token developers. Many scripts would likely be reusable (especially in case of contract factories).
Where to store parser scripts
It seems logical to keep them on-chain since they are actually valid contracts. FA token contracts can keep the parser address (pointer) in the storage.
Could there be multiple parsers
The approach can be generalized to extract any other data and not just from big_map_diff
, but also from contract storage, operation parameters. You can basically move all view methods that are not used by other contracts off-chain, make them external (as suggested in [1]).
What if it’s impossible to calculate balances using just big_map_diff
?
In cases when the holders’ balances are calculated dynamically (or else) this approach won’t work and one would require full-fledged external views (see [1], [2]). However, big_map_diff
still shoud be considered as a source of info which paricular accounts were altered.
Proof-of-concept
A parser script for the TZBTC contract.
Check out the step-by-step tutorial:
- Static
https://nbviewer.jupyter.org/github/baking-bad/michelson-kernel/blob/binder/tzbtc_big_map_diff_parser.ipynb - Interactive
https://mybinder.org/v2/gh/baking-bad/michelson-kernel/binder?filepath=tzbtc_big_map_diff_parser.ipynb
NOTE: This is probably the hardest case one could imagine, most scripts would be much much simpler.
A balance_updates derivation demo
Implemented using the TZBTC parser script and PyTezos library:
- Static
https://gist.github.com/m-kus/a9081b75d91d6cba9ed1cfe8b1b59e40 - Interactive
https://colab.research.google.com/drive/1ZHx0OdmPg2slTqhzJSfYL0yR2VC_sS9b?usp=sharing
Inspired by
[1] External Views by Gabriel Alfour
[2] https://smondet.gitlab.io/fa2-smartpy/tutorial.html#get-balance-off-chain by Sebastien Mondet