2. Tracking ERC721 owner changes

We build APIs for recent Nouns transfers and a leaderboard of most popular NFTs (by owner change counts)

Let's index ERC721s / NFTs, tracking any instance in which one is minted/burned or changes hands. In this canvas, we'll hook on all contracts that implement the ERC721 standard and track changes to the owners variable in storage.

The ERC721 standard actually includes three transfer functions: transferFrom, and two variants of safeTransferFrom. They all impact the same storage variables. Mint and burn functions, while common, are not defined in the standard--different contracts implement them in different ways. But because they impact the same storage variables, we'll catch those too so long as we hook on storage.

If you want to skip to the end, here's the canvas that already has everything built.


Steps

Build the pipeline

  1. Add the following components in order and connect them: Data source, EVM lambda, Schema, Persistence.

  2. In the data source component, set the From in the block range to 1. While the ERC721 standard was formally accepted in early 2018, we'll run from genesis to include contracts that implemented the standard before its acceptance. Leave the To blank to let it keep running at the tip.

  3. Name both the schema and persistence components as erc721_owner_changes. Populate the schema as follows, then save the schema and set the persistence.

    NameType
    txn_hashbytes32
    global_counteruint256
    block_numberuint256
    token_addressaddress
    iduint256
    owner_beforeaddress
    owner_afteraddress

Define the EVM lambda

  1. Open the IDE in the EVM lambda component. In the hooks sidebar, select Classification under Hook type and then ERC721. Any hook we select under this category will target all contracts whose ABI includes that of the ERC721 standard.
  2. Add an erc721_owners storage post hook, and name the callback postOwnerChange. For the callback, we're just going to emit some very simple data upon each change in owner.
    function postOwnerChange() public {
        postOwnerChangeContext storage ctx = getPostOwnerChangeContext();
        simEmitToSchema_erc721_owner_changes(
            ctx.txn.hash, // txn_hash
            simGlobalCounter(), // global_counter
            block.number, // block_number
            ctx.txn.to, // token_address
            ctx.path.tokenId, // id
            ctx.valueBefore, // owner_before
            ctx.valueAfter // owner_after
        );
    }
  1. Test the execution using the button at the top-right of the IDE. Just about any somewhat recent block range will involve some ERC721 transfers.
  2. Now that we've tested, leave the IDE and hit the play button on the execution edge. In a few moments, we should see both tip and backfill executions in the execution edge status.

Querying and building APIs

You can prototype queries using the query editor. Let's use some we've prebaked to make APIs.

  1. Add an API component to the canvas. It doesn't have to be connected to any other components. For the first API, let's do a simple one that shows the N most recent Nouns owner changes. Use the following query:
select * from @org.erc721_owner_changes
where token_address = '0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03'
order by global_counter desc
limit $limit
  1. For the limit parameter, use type uint32 (a few others would work too), and set the default value to 1.
  2. Test the query using the test interaction, then set the API, activate it, and try the cURL request available in the actions menu.
  3. For our second API, let's build a leaderboard of the most popular NFT contracts over a user-defined block range, sorting by the number of owner changes. Here's a query for that:
with most_recent_block as (
    select max(cast(block_number as double)) as most_recent_block from @org.erc721_owner_changes
)
select 
    token_address,
    count(*) as cnt
from @org.erc721_owner_changes
where cast(block_number as double) >= (select * from most_recent_block) - $block_range
group by token_address
order by cnt desc
limit $limit
  1. Both block_range and limit parameters can be typed as uint32, and 1000 and 5 would make sense as default parameters. Again, test the query and API using the test feature and the cURL.

Key takeaways

  1. You can easily hook on all contracts that implement a certain standard, like ERC721, even if they have vastly different implementations.
  2. Hooking on storage allows us to abstract from differences in function and log implementations.
  3. You can query and build APIs against all of your persistences as well as sim core tables from any canvas.

Join our Telegram