Protobuf
Rust code generated, let's write our handler code in src/lib.rs
as such:constant
, and initiate our Ethereum Substreamsmap
module. As a reminder, here is the module definition in the Manifiest that we created:name: block_to_transfers
. This name should correspond to our handler function name.sf.ethereum.type.v1.Block
which is a standard Ethereum block provided by the substreams-ethereum
crate. The output has a type of proto:eth.erc721.v1.Transfers
which is our custom Protobuf
definition and is provided by the generated Rust code we did in the prior steps. This yields the following function signature:#[substreams::handlers::map]
above the function, this is a rust macro that is provided by the substreams
crate. This macro decorates our handler function as a map
. There is also a macro used to decorate handler of kind store
:#[substreams::handlers::store]
map
we are building is to extract ERC721
Transfers from a given block. We can achieve this by finding all the Transfer
events that are emitted by the contract we are tracking. Once we find such an event we will decode it and create a Transfer
objectstore
module. As a reminder, here is the module definition in the Manifiestname: nft_state
. This name should also correspond to our handler function name.map
module block_to_transfers
, which is of type proto:eth.erc721.v1.Transfers
. This is our custom Protobuf
definition and is provided by the generated Rust code we did in the prior steps. This yields the following function signature:store
will always take as its last input the writable store itself. In this example the store
module has an updatePolicy: add
and a valueType: int64
this yields a writable store of type StoreAddInt64
store
module function should always be the writable store itself. The type of said writable store is based on your store
module updatePolicy
and valueType
. You can see all the possible types of store here.store
we are building is to keep track of a holder's current NFT count for the given contract. We will achieve this by analyzing the transfers.from
address field is the null address (0x0000000000000000000000000000000000000000
) and the to
address field is not the null address, we know the to
address field is minting a token, and we should increment his count.from
address field is not the null address and the to
address field is the null address, we know the from
address field is burning a token, and we should decrement his count.from
address field and the to
address field is not the null address, we should decrement the count of the from
address and increment the count of the to
address field as this is a basic transfer.ordinal
: this represents the order in which your store
operations will be applied. Consider the following: your store
handler will be called once per block
- during that execution it may call the add
operation multiple times, for multiple reasons (found a relevant event, saw a call that triggered a method call). Since a blockchain execution model is linear and deterministic, we need to make sure we can apply your add
operations linearly and deterministically. By having to specify an ordinal, we can guarantee the order of execution. In other words, given one execution of your store
handler for given inputs (in this example a list of transfers), your code should emit the same number of add
calls with the same ordinal values.key
: Since our stores are key/value stores, we need to take care in crafting the key, to ensure that it is unique and flexible. In our example, if the generate_key
function would simply return a key that is the TRACKED_CONTRACT
address it would not be unique between different token holders. If the generate_key
function would return a key that is only the holder's address, though it would be unique amongst holders, we would run into issues if we wanted to track multiple contracts.value
: The value we are storing, the type is dependant on the store type we are using.